C#
C#
Documentación de C#
Primeros pasos
Introducción
Tipos
Bloques de creación de programas
Áreas principales del lenguaje
Tutoriales
Elección de la primera lección
Tutoriales basados en el explorador
Hola a todos
Números en C#
Bifurcaciones y bucles
Colecciones de listas
Trabajo en un entorno local
Configuración del entorno
Números en C#
Bifurcaciones y bucles
Colecciones de listas
Aspectos básicos
Estructura del programa
Información general
Main (método)
Instrucciones de nivel superior
Sistema de tipos
Información general
Espacios de nombres
Clases
Registros
Interfaces
Genéricos
Tipos anónimos
Programación orientada a objetos
Información general
de la empresa
Herencia
Polimorfismo
Técnicas funcionales
Detección de patrones
Descartes
Deconstruir tuplas y otros tipos
Excepciones y errores
Información general
Uso de excepciones
Control de excepciones
Creación y producción de excepciones
Excepciones generadas por el compilador
Estilo de codificación
Nombres de identificador
Convenciones de código de C#
Tutoriales
Procedimiento para mostrar argumentos de la línea de comandos
Introducción a las clases
C# orientado a objetos
Herencia en C# y .NET
Conversión de tipos
Creación de algoritmos controlados por datos con coincidencia de patrones
Procedimiento para controlar una excepción mediante Try y Catch
Procedimiento para ejecutar código de limpieza mediante finally
Novedades de C#
C# 10.0 (versión preliminar 7)
C# 9.0
C# 8.0
C# 7.0-7.3
Cambios importantes del compilador
Historial de versiones de C#
Relaciones con la biblioteca de .NET
Compatibilidad de versiones
Tutoriales
Exploración de los tipos de registros
Expliración de instrucciones de nivel superior
Exploración de patrones en objetos
Actualización segura de la interfaz con métodos de la predeterminada
Creación de la funcionalidad Mixin con métodos de interfaz predeterminados
Exploración de los índices y rangos
Trabajo con tipos de referencias que aceptan valores NULL
Actualización de aplicaciones a tipos de referencias que aceptan valores NULL
Generación y consumo de flujos asincrónicos
Tutoriales
Exploración de la interpolación de cadenas: tutorial interactivo
Exploración de la interpolación de cadenas: tutorial en el entorno
Escenarios avanzados de la interpolación de cadenas
Aplicación de consola
Cliente REST
Uso de LINQ
Uso de atributos
Conceptos de C#
Tipos de referencia que aceptan valores NULL
Elección de una estrategia para habilitar tipos de referencia que acepten valores NULL
Métodos
Propiedades
Indizadores
Iterators
Delegados y eventos
Introducción a los delegados
System.Delegate y la palabra clave delegate
Delegados fuertemente tipados
Patrones comunes para delegados
Introducción a los eventos
Patrón de eventos estándar de .NET
Patrón de eventos actualizado de .NET
Distinción de delegados y eventos
Language-Integrated Query (LINQ)
Información general de LINQ
Conceptos básicos de las expresiones de consultas
LINQ en C#
Escribir consultas LINQ en C#
Consultar una colección de objetos
Devolver una consulta de un método
Almacenar los resultados de una consulta en memoria
Agrupar los resultados de consultas
Crear un grupo anidado
Realizar una subconsulta en una operación de agrupación
Agrupar resultados por claves contiguas
Especificar dinámicamente filtros con predicado en tiempo de ejecución
Realizar combinaciones internas
Realizar combinaciones agrupadas
Realizar operaciones de combinación externa izquierda
Ordenar los resultados de una cláusula join
Realizar una unión usando claves compuestas
Realizar operaciones de combinación personalizadas
Controlar valores nulos en expresiones de consulta
Controlar excepciones en expresiones de consulta
Escritura de código seguro y eficaz
Árboles de expresión
Introducción a los árboles de expresión
Árboles de expresiones en detalle
Tipos de marco que admiten árboles de expresión
Ejecución de expresiones
Interpretación de expresiones
Generación de expresiones
Traducción de expresiones
Resumen
Interoperabilidad nativa
Control de versiones
Artículos de procedimientos de C#
Índice de artículos
División de cadenas en subcadenas
Concatenación de cadenas
Búsqueda en las cadenas
Modificación del contenido de las cadenas
Comparación de cadenas
Procedimiento para detectar excepciones no compatibles con CLS
SDK de .NET Compiler Platform (API de Roslyn)
Información general del SDK de .NET Compiler Platform (API de Roslyn)
Descripción del modelo de API de compilador
Trabajar con sintaxis
Trabajar con semántica
Trabajar con un área de trabajo
Exploración de código con el visualizador de sintaxis
Generadores de origen
Guías rápidas
Análisis de sintaxis
Análisis semántico
Transformación de sintaxis
Tutoriales
Crear el primer analizador y la corrección de código
Guía de programación de C#
Información general
Información general
Conceptos de programación
Información general
Programación asincrónica
Información general
Escenarios de programación asincrónica
Modelo de programación asincrónica de tareas
Tipos de valores devueltos asincrónicos
Cancelación de tareas
Cancelar una lista de tareas
Cancelación de tareas tras un período de tiempo
Procesamiento de tareas asincrónicas a medida que se completan
Acceso asincrónico a archivos
Atributos
Información general
Crear atributos personalizados
Acceso a atributos mediante reflexión
Procedimiento para: Crear una unión de C/C++ mediante atributos
Colecciones
Covarianza y contravarianza
Información general
Varianza en interfaces genéricas
Creación de interfaces genéricas variantes
Uso de la varianza en interfaces para las colecciones genéricas
Varianza en delegados
Uso de varianza en delegados
Uso de varianza para los delegados genéricos Func y Action
Árboles de expresión
Información general
Ejecución de árboles de expresión
Modificación de árboles de expresión
Uso de árboles de expresión para crear consultas dinámicas
Depuración de árboles de expresión en Visual Studio
Sintaxis de DebugView
Iterators
Language-Integrated Query (LINQ)
Información general
Introducción a LINQ en C#
Introducción a las consultas LINQ
LINQ y tipos genéricos
Operaciones básicas de consulta LINQ
Transformaciones de datos con LINQ
Relaciones entre tipos en las operaciones de consulta LINQ
Sintaxis de consulta y sintaxis de método en LINQ
Características de C# compatibles con LINQ
Tutorial: Escribir consultas en C# (LINQ)
Información general sobre operadores de consulta estándar
Información general
Sintaxis de las expresiones de consulta para operadores de consulta estándar
Clasificación de operadores de consulta estándar por modo de ejecución
Ordenación de datos
Operaciones Set
Filtrado de datos
Operaciones cuantificadoras
Operaciones de proyección
Realización de particiones de datos
Operaciones de combinación
Agrupar datos
Operaciones de generación
Operaciones de igualdad
Operaciones de elementos
Conversión de tipos de datos
Operaciones de concatenación
Operaciones de agregación
LINQ to Objects
Información general
LINQ y cadenas
Artículos de procedimientos
Procedimiento para: Realizar un recuento de las repeticiones de una palabra en
una cadena (LINQ)
Procedimiento para buscar frases que contengan un conjunto especificado de
palabras (LINQ)
Procedimiento para consultar caracteres en una cadena (LINQ)
Procedimiento para: Combinar consultas LINQ con expresiones regulares
Procedimiento para buscar la diferencia de conjuntos entre dos listas (LINQ)
Procedimiento para ordenar o filtrar datos de texto por palabra o campo
(LINQ)
Procedimiento para reordenar los campos de un archivo delimitado (LINQ)
Procedimiento para: Combinar y comparar colecciones de cadenas (LINQ)
Procedimiento para rellenar colecciones de objetos de varios orígenes (LINQ)
Procedimiento para dividir un archivo en varios mediante el uso de grupos
(LINQ)
Procedimiento para combinar contenido de archivos no similares (LINQ)
Procedimiento para: Calcular valores de columna en un archivo de texto CSV
(LINQ)
LINQ y Reflection
Procedimiento para consultar los metadatos de un ensamblado con la función de
reflexión (LINQ)
LINQ y directorios de archivos
Información general
Procedimiento para buscar archivos con un nombre o atributo especificados
Procedimiento para agrupar archivos por extensión (LINQ)
Procedimiento para consultar el número total de bytes en un conjunto de
carpetas (LINQ)
Procedimiento para: Comparar el contenido de dos carpetas (LINQ)
Procedimiento para consultar el archivo o archivos de mayor tamaño en un
árbol de directorios (LINQ)
Procedimiento para consultar archivos duplicados en un árbol de directorios
(LINQ)
Cómo: Consultar el contenido de los archivos de una carpeta (LINQ)
Procedimiento para consultar un objeto ArrayList con LINQ
Procedimiento para: Agregar métodos personalizados para las consultas LINQ
LINQ to ADO.NET (Página de portal)
Habilitar un origen de datos para realizar consultas LINQ
Características de IDE de Visual Studio y herramientas para LINQ
Reflexión
Serialización (C#)
Información general
Procedimiento para escribir objetos de datos en un archivo XML
Procedimiento para leer objetos de datos de un archivo XML
Tutorial: Conservar un objeto en Visual Studio
Instrucciones, expresiones y operadores
Información general
Instrucciones
Miembros con forma de expresión
Funciones anónimas
Información general
Procedimiento para usar expresiones lambda en una consulta
Igualdad y comparaciones de igualdad
Comparaciones de igualdad
Procedimiento para definir la igualdad de valores para un tipo
Procedimiento para probar la igualdad de referencia (Identidad)
Tipos
Conversiones de tipos
Conversión boxing y conversión unboxing
Procedimiento para convertir una matriz de bytes en un valor int
Procedimiento para convertir una cadena en un número
Procedimiento para convertir cadenas hexadecimales en tipos numéricos
Uso de tipo dinámico
Tutorial: Crear y usar objetos dinámicos (C# y Visual Basic)
Clases, estructuras y registros
Polimorfismo
Control de versiones con las palabras clave Override y New
Saber cuándo utilizar las palabras clave Override y New
Invalidación del método ToString
Miembros
Información general sobre los miembros
Clases y miembros de clase abstractos y sellados
Clases estáticas y sus miembros
Modificadores de acceso
Campos
Constantes
Definición de propiedades abstractas
Definición de constantes en C#
Propiedades
Información general sobre propiedades
Utilizar propiedades
Propiedades de interfaz
Restringir la accesibilidad del descriptor de acceso
Declaración y uso de propiedades de lectura y escritura
Propiedades autoimplementadas
Cómo implementar una clase ligera con propiedades autoimplementadas
Métodos
Información general sobre los métodos
Funciones locales
Valores devueltos y variables locales de tipo ref
Parámetros
Pasar parámetros
Pasar parámetros de tipo de valor
Pasar parámetros Reference-Type
Conocimiento de las diferencias entre pasar a un método un struct y una
referencia a clase
Variables locales con asignación implícita de tipos
Uso de matrices y variables locales con tipo implícito en expresiones de consulta
Métodos de extensión.
Implementación e invocación de un método de extensión personalizado
Creación de un método nuevo para una enumeración
Argumentos opcionales y con nombre
Uso de argumentos opcionales y con nombre en la programación de Office
Constructores
Información general sobre los constructores
Utilizar constructores
Constructores de instancias
Constructores privados
Constructores estáticos
Escritura de un constructor copy
Finalizadores
Inicializadores de objeto y colección
Procedimiento para inicializar un objeto mediante un inicializador de objeto
Procedimiento para inicializar un diccionario con un inicializador de colección
Tipos anidados
Clases y métodos parciales
Devolución de subconjuntos de propiedades de elementos en una consulta
Interfaces
Implementación explícita de interfaz
Procedimiento para implementar miembros de interfaz de forma explícita
Procedimiento para implementar miembros de dos interfaces de forma explícita
Delegados
Información general
Utilizar delegados
Delegados con métodos con nombre y Métodos anónimos
Procedimiento para combinar delegados (delegados de multidifusión) (Guía de
programación de C#)
Procedimiento para declarar un delegado, crear instancias del mismo y usarlo
Matrices
Información general
Matrices unidimensionales
Matrices multidimensionales
Matrices escalonadas
Usar foreach con matrices
Pasar matrices como argumentos
Matrices con tipo implícito
Cadenas
Programación con cadenas
Procedimiento para determinar si una cadena representa un valor numérico
Indizadores
Información general
Utilizar indizadores
Indizadores en interfaces
Comparación entre propiedades e indizadores
Events
Información general
Procedimiento para suscribir y cancelar la suscripción a eventos
Procedimiento para publicar eventos que cumplan las instrucciones de .NET
Procedimiento para generar eventos de una clase base en clases derivadas
Procedimiento para implementar eventos de interfaz
Procedimiento para implementar descriptores de acceso de eventos personalizados
Genéricos
Parámetros de tipos genéricos
Restricciones de tipos de parámetros
Clases genéricas
Interfaces genéricas
Métodos genéricos
Genéricos y matrices
Delegados genéricos
Diferencias entre plantillas de C++ y tipos genéricos de C#
Genéricos en el tiempo de ejecución
Genéricos y reflexión
Genéricos y atributos
Espacios de nombres
Utilizar espacios de nombres
Procedimiento para usar el espacio de nombres My
Registro y sistema de archivos
Información general
Procedimiento para recorrer en iteración un árbol de directorio
Procedimiento para obtener información sobre archivos, carpetas y unidades
Procedimiento para crear archivos o carpetas
Procedimiento para copiar, eliminar y mover archivos y carpetas
Procedimiento para proporcionar un cuadro de diálogo de progreso para
operaciones de archivos
Procedimiento para escribir en un archivo texto
Procedimiento para leer de un archivo de texto
Procedimiento para leer un archivo de texto línea a línea
Procedimiento para crear una clave en el Registro
Interoperabilidad
Interoperabilidad de .NET
Información general sobre interoperabilidad
Procedimiento para acceder a objetos de interoperabilidad de Office mediante
características de C#
Procedimiento para usar propiedades indizadas en la programación de
interoperabilidad COM
Procedimiento para usar la invocación de plataforma para reproducir un archivo
WAV
Tutorial: Programación de Office (C# y Visual Basic)
Clase COM de ejemplo
Referencia del lenguaje
Información general
Configurar la versión del lenguaje
Tipos
Tipos de valor
Información general
Tipos numéricos integrales
Tipos de enteros nativos nint y nuint
Tipos numéricos de punto flotante
Conversiones numéricas integradas
bool
char
Tipos de enumeración
Tipos de estructura
Tipos de tupla
Tipos de valor que aceptan valores NULL
Tipos de referencia
Características de los tipos de referencia
Tipos de referencia integrados
registro
clase
interfaz
Tipos de referencia que aceptan valores NULL
void
var
Tipos integrados
Tipos no administrados
Valores predeterminados
Palabras clave
Información general
Modificadores
Modificadores de acceso
Referencia rápida
Niveles de accesibilidad
Dominio de accesibilidad
Restricciones en el uso de los niveles de accesibilidad
internal
private
protected
public
protected internal
privado protegido
abstract
async
const
event
extern
in (modificador genérico)
new (modificador de miembro)
out (modificador genérico)
override
readonly
sealed
static
unsafe
virtual
volatile
Palabras clave de instrucciones
Categorías de instrucciones
Instrucciones de salto
break
continue
goto
return
Instrucciones para el control de excepciones
throw
try-catch
try-finally
try-catch-finally
Checked y unchecked
Información general
checked
unchecked
Instrucción fixed
Instrucción lock
Parámetros de métodos
Pasar parámetros
params
in (modificador de parámetro)
ref
out (modificador de parámetro)
Palabras clave del espacio de nombres
namespace
using
Contextos de uso
using (directiva)
Instrucción using
alias externo
Palabras clave de restricción de tipo genérico
Restricción new
where
Palabras clave de acceso
base
this
Palabras clave de literales
null
true y false
default
Palabras clave contextuales
Referencia rápida
add
get
init
partial (Tipos)
partial (método)
remove
set
when (condición de filtro)
value
yield
Palabras clave para consultas
Referencia rápida
from (cláusula)
where (cláusula)
select (cláusula)
group (cláusula)
into
orderby (cláusula)
join (cláusula)
let (cláusula)
ascending
descending
on
equals
by
in
Operadores y expresiones
Información general
Operadores aritméticos
Operadores lógicos booleanos
Operadores de desplazamiento y bit a bit
Operadores de igualdad
Operadores de comparación
Operadores y expresiones de acceso a miembros
Operadores de prueba de tipos y expresión de conversión
Operadores de conversión definidos por el usuario
Operadores relacionados con punteros
Operadores de asignación
Expresiones lambda
Patrones
+ Operadores and +=
- Operadores and -=
Operador ?:
! (permite valores NULL)
?? Operadores and ??=
Operador =>
:: (operador)
await (operador)
Expresiones de valor predeterminado
operador delegate
Operador is
Expresión nameof
Operador new
Operador sizeof
Expresión stackalloc
Expresión switch
operadores true y false
Expresión with
Sobrecarga de operadores
Instrucciones
Instrucciones de iteración
Instrucciones de selección
Caracteres especiales
Información general
$ -- interpolación de cadenas
@ -- identificador textual
Atributos leídos por el compilador
Atributos globales
Información del agente de llamada
Análisis estático que admite un valor NULL
Varios
Código no seguro y punteros
Directivas de preprocesador
Opciones del compilador
Información general
Opciones del lenguaje
Opciones de salida
Opciones de entrada
Opciones de los errores y advertencias
Opciones de la generación del código
Opciones de la seguridad
Opciones de los recursos
Otras opciones
Opciones avanzadas
Comentarios de la documentación XML
Generación de documentación de API
Etiquetas recomendadas
Ejemplos
Mensajes del compilador
Especificaciones
Borrador de especificación de C# 6.0
Introducción
Estructura léxica
Conceptos básicos
Tipos
variables
Conversiones
Expresiones
Instrucciones
Espacios de nombres
Clases
Estructuras
Matrices
Interfaces
Enumeraciones
Delegados
Excepciones
Atributos
Código no seguro
Comentarios de documentación
Características de C# 7.0-10.0
Características de C# 7.0
Detección de patrones
Funciones locales
Declaraciones de variable out
Expresiones throw
Literales binarios
Separadores de dígitos
Tipos de tarea asincrónica
Características de C# 7.1
Método async main
Expresiones predeterminadas
Inferir nombres de tupla
Coincidencia de patrones con genéricos
Características de C# 7.2
Referencias readonly
Seguridad de tiempo de compilación para tipos similares a ref
Argumentos con nombre no finales
Private protected
Ref condicional
Separador de dígito inicial
Características de C# 7.3
Restricciones de tipo genérico no administrado
Los campos de indexación `fixed` no deberían requerir anclaje
independientemente del contexto movible/inamovible
Instrucción `fixed` basada en patrones
Reasignación de referencias locales
Inicializadores de matriz stackalloc
Atributos de campo de destino de propiedad implementada automáticamente
Variables de expresión en inicializadores
Igualdad (==) y desigualdad (!=) de tuplas
Mejoras en los candidatos de sobrecarga
Características de C# 8.0
Tipos de referencia que aceptan valores NULL: propuesta
Tipos de referencias que aceptan valores NULL: especificación
Coincidencia de patrones recursiva
Métodos de interfaz predeterminados
Flujos asincrónicos
Intervalos
using basado en patrón y declaraciones using
Funciones locales estáticas
Asignación de uso combinado de NULL
Miembros de instancia de solo lectura
Elemento stackalloc anidado
Características de C# 9.0
Registros
Instrucciones de nivel superior
Tipos de referencias que aceptan valores NULL: especificación
Mejoras de coincidencia de patrones
Establecedores de solo inicialización
Expresiones nuevas con tipo de destino
Inicializadores de módulo
Extender métodos parciales
Funciones anónimas estáticas
Expresión condicional con tipo de destino
Tipos de valor devueltos de covariante
Extensión GetEnumerator en bucles foreach
Parámetros de descarte lambda
Atributos en funciones locales
Enteros con tamaño nativos
Punteros de función
Supresión de la emisión de la marca localsinit
Anotaciones de parámetros de tipo sin restricciones
Características de C# 10.0
Structs de registro
Constructores de structs sin parámetros
Directiva using global
Espacios de nombres con ámbito de archivo
Patrones de propiedades extendidos
Cadenas interpoladas mejoradas
Cadenas interpoladas constantes
Mejoras de lambda
Atributos de lambda
Expresión de argumentos del autor de la llamada
Directivas #line mejoradas
Atributos genéricos
Análisis mejorado de asignación definitiva
Invalidación de AsyncMethodBuilder
Paseo por el lenguaje C#
16/09/2021 • 16 minutes to read
C# (pronunciado "si sharp" en inglés) es un lenguaje de programación moderno, basado en objetos y con
seguridad de tipos. C# permite a los desarrolladores crear muchos tipos de aplicaciones seguras y sólidas que
se ejecutan en .NET. C# tiene sus raíces en la familia de lenguajes C, y a los programadores de C, C++, Java y
JavaScript les resultará familiar inmediatamente. Este paseo proporciona información general de los principales
componentes del lenguaje en C# 8 y versiones anteriores. Si quiere explorar el lenguaje a través de ejemplos
interactivos, pruebe los tutoriales de introducción a C#.
C# es un lenguaje de programación *orientado a componentes _, orientado a objetos. C# proporciona
construcciones de lenguaje para admitir directamente estos conceptos, por lo que se trata de un lenguaje
natural en el que crear y usar componentes de software. Desde su origen, C# ha agregado características para
admitir nuevas cargas de trabajo y prácticas de diseño de software emergentes. En su núcleo, C# es un lenguaje
_ *orientado a objetos**. Defina los tipos y su comportamiento.
Varias características de C# facilitan la creación de aplicaciones sólidas y duraderas. La *recolección de
elementos no utilizados _ reclama de forma automática la memoria ocupada por objetos no utilizados
inalcanzables. Los tipos que aceptan valores NULL ofrecen protección ante variables que no hacen referencia a
objetos asignados. El control de excepciones proporciona un enfoque estructurado y extensible para la detección
y recuperación de errores. Las expresiones lambda admiten técnicas de programación funcional. La sintaxis de
Language Integrated Query (LINQ) crea un patrón común para trabajar con datos de cualquier origen. La
compatibilidad del lenguaje con las operaciones asincrónicas proporciona la sintaxis para crear sistemas
distribuidos. C# tiene un _ *sistema de tipos unificados**. Todos los tipos de C#, incluidos los tipos primitivos
como int y double , se heredan de un único tipo object raíz. Todos los tipos comparten un conjunto de
operaciones comunes. Los valores de cualquier tipo se pueden almacenar, transportar y operar de forma
coherente. Además, C# admite tanto tipos de referencia definidos por el usuario como tipos de valor. C# permite
la asignación dinámica de objetos y el almacenamiento en línea de estructuras ligeras. C# admite métodos y
tipos genéricos, que proporcionan una mayor seguridad de tipos, así como un mejor rendimiento. C# también
proporciona iteradores, gracias a los que los implementadores de clases de colecciones pueden definir
comportamientos personalizados para el código de cliente.
C# resalta el control de versiones para asegurarse de que los programas y las bibliotecas pueden evolucionar
con el tiempo de manera compatible. Los aspectos del diseño de C# afectados directamente por las
consideraciones de versionamiento incluyen los modificadores virtual y override independientes, las reglas
para la resolución de sobrecargas de métodos y la compatibilidad para declaraciones explícitas de miembros de
interfaz.
Arquitectura de .NET
Los programas de C# se ejecutan en .NET, un sistema de ejecución virtual denominado Common Language
Runtime (CLR) y un conjunto de bibliotecas de clases. CLR es la implementación de Microsoft del estándar
internacional Common Language Infrastructure (CLI). CLI es la base para crear entornos de ejecución y
desarrollo en los que los lenguajes y las bibliotecas funcionan juntos sin problemas.
El código fuente escrito en C# se compila en un lenguaje intermedio (IL) que guarda conformidad con la
especificación de CLI. El código y los recursos de IL, como los mapas de bits y las cadenas, se almacenan en un
ensamblado, normalmente con una extensión .dll. Un ensamblado contiene un manifiesto que proporciona
información sobre los tipos, la versión y la referencia cultural.
Cuando se ejecuta el programa C#, el ensamblado se carga en CLR. CLR realiza la compilación Just-In-Time (JIT)
para convertir el código IL en instrucciones de máquina nativas. Además, CLR proporciona otros servicios
relacionados con la recolección de elementos no utilizados, el control de excepciones y la administración de
recursos. El código que se ejecuta en el CLR se conoce a veces como "código administrado", a diferencia del
"código no administrado", que se compila en un lenguaje nativo de la máquina destinado a un sistema
específico.
La interoperabilidad entre lenguajes es una característica principal de .NET. El código IL generado por el
compilador de C# se ajusta a la especificación de tipo común (CTS). El código IL generado desde C# puede
interactuar con el código generado a partir de las versiones de .NET de F# , Visual Basic, C++ o cualquiera de los
más de 20 lenguajes compatibles con CTS. Un solo ensamblado puede contener varios módulos escritos en
diferentes lenguajes .NET y los tipos se pueden hacer referencia mutuamente igual que si estuvieran escritos en
el mismo lenguaje.
Además de los servicios en tiempo de ejecución, .NET también incluye amplias bibliotecas, que admiten muchas
cargas de trabajo diferentes. Se organizan en espacios de nombres que proporcionan una gran variedad de
funciones útiles para todo, desde la entrada y salida de archivos, la manipulación de cadenas y el análisis de
XML hasta los marcos de aplicaciones web y los controles de Windows Forms. En una aplicación de C# típica se
usa la biblioteca de clases de .NET de forma extensa para controlar tareas comunes de infraestructura.
Para obtener más información sobre .NET, vea Introducción a .NET.
Hola a todos
El programa "Hola mundo" tradicionalmente se usa para presentar un lenguaje de programación. En este caso,
se usa C#:
using System;
class Hello
{
static void Main()
{
Console.WriteLine("Hello, World");
}
}
El programa "Hola mundo" empieza con una directiva using que hace referencia al espacio de nombres
System . Los espacios de nombres proporcionan un método jerárquico para organizar las bibliotecas y los
programas de C#. Los espacios de nombres contienen tipos y otros espacios de nombres; por ejemplo, el
espacio de nombres System contiene varios tipos, como la clase Console a la que se hace referencia en el
programa, y otros espacios de nombres, como IO y Collections . Una directiva using que hace referencia a
un espacio de nombres determinado permite el uso no calificado de los tipos que son miembros de ese espacio
de nombres. Debido a la directiva using , puede utilizar el programa Console.WriteLine como abreviatura de
System.Console.WriteLine .
La clase Hello declarada por el programa "Hola mundo" tiene un miembro único, el método llamado Main . El
método Main se declara con el modificador static . Mientras que los métodos de instancia pueden hacer
referencia a una instancia de objeto envolvente determinada utilizando la palabra clave this , los métodos
estáticos funcionan sin referencia a un objeto determinado. Por convención, un método estático denominado
Main sirve como punto de entrada de un programa de C#.
La salida del programa la genera el método WriteLine de la clase Console en el espacio de nombres System .
Esta clase la proporcionan las bibliotecas de clase estándar, a las que, de forma predeterminada, el compilador
hace referencia automáticamente.
Tipos y variables
Un tipo define la estructura y el comportamiento de los datos en C#. La declaración de un tipo puede incluir sus
miembros, tipo base, interfaces que implementa y operaciones permitidas para ese tipo. Una variable es una
etiqueta que hace referencia a una instancia de un tipo específico.
Hay dos clases de tipos en C#: tipos de valor y tipos de referencia. Las variables de tipos de valor contienen
directamente sus datos. Las variables de tipos de referencia almacenan referencias a los datos, lo que se conoce
como objetos. Con los tipos de referencia, es posible que dos variables hagan referencia al mismo objeto y que,
por tanto, las operaciones en una variable afecten al objeto al que hace referencia la otra. Con los tipos de valor,
cada variable tiene su propia copia de los datos y no es posible que las operaciones en una variable afecten a la
otra (excepto para las variables de parámetro ref y out ).
Un identificador es un nombre de variable. Un identificador es una secuencia de caracteres Unicode sin ningún
espacio en blanco. Un identificador puede ser una palabra reservada de C# si tiene el prefijo @ . El uso de una
palabra reservada como identificador puede ser útil al interactuar con otros lenguajes.
Los tipos de valor de C# se dividen en tipos simples, tipos de enumeración, tipos de estructura, tipos de valor
que aceptan valores NULL y tipos de valor de tupla. Los tipos de referencia de C# se dividen en tipos de clase,
tipos de interfaz, tipos de matriz y tipos delegados.
En el esquema siguiente se ofrece información general del sistema de tipos de C#.
Tipos de valor
Tipos simples
Entero con signo: sbyte , short , int , long
Entero sin signo: byte , ushort , uint , ulong
Caracteres Unicode: char , que representa una unidad de código UTF-16
Punto flotante binario IEEE: float , double
Punto flotante decimal de alta precisión: decimal
Booleano: bool , que representa valores booleanos, valores que son true o false
Tipos de enumeración
Tipos definidos por el usuario con el formato enum E {...} . Un tipo enum es un tipo distinto
con constantes con nombre. Cada tipo enum tiene un tipo subyacente, que debe ser uno de los
ocho tipos enteros. El conjunto de valores de un tipo enum es igual que el conjunto de valores
del tipo subyacente.
Tipos de estructura
Tipos definidos por el usuario con el formato struct S {...}
Tipos de valores que aceptan valores NULL
Extensiones de todos los demás tipos de valor con un valor null
Tipos de valor de tupla
Tipos definidos por el usuario con el formato (T1, T2, ...)
Tipos de referencia
Tipos de clase
Clase base definitiva de todos los demás tipos: object
Cadenas Unicode: string , que representa una secuencia de unidades de código UTF-16
Tipos definidos por el usuario con el formato class C {...}
Tipos de interfaz
Tipos definidos por el usuario con el formato interface I {...}
Tipos de matriz
Unidimensional, multidimensional y escalonada. Por ejemplo, int[] , int[,] y int[][] .
Tipos delegados
Tipos definidos por el usuario con el formato delegate int D(...)
Los programas de C# utilizan declaraciones de tipos para crear nuevos tipos. Una declaración de tipos especifica
el nombre y los miembros del nuevo tipo. Seis de las categorías de tipos de C# las define el usuario: tipos de
clase, tipos de estructura, tipos de interfaz, tipos de enumeración, tipos de delegado y tipos de valor de tupla.
A tipo class define una estructura de datos que contiene miembros de datos (campos) y miembros de
función (métodos, propiedades y otros). Los tipos de clase admiten herencia única y polimorfismo,
mecanismos por los que las clases derivadas pueden extender y especializar clases base.
Un tipo struct es similar a un tipo de clase, por el hecho de que representa una estructura con miembros
de datos y miembros de función. Pero a diferencia de las clases, las estructuras son tipos de valor y no suelen
requerir la asignación del montón. Los tipos de estructura no admiten la herencia especificada por el usuario
y todos se heredan implícitamente del tipo object .
Un tipo interface define un contrato como un conjunto con nombre de miembros públicos. Un valor
class o struct que implementa interface debe proporcionar implementaciones de miembros de la
interfaz. Un interface puede heredar de varias interfaces base, y un class o struct pueden implementar
varias interfaces.
Un tipo delegate representa las referencias a métodos con una lista de parámetros determinada y un tipo
de valor devuelto. Los delegados permiten tratar métodos como entidades que se puedan asignar a variables
y se puedan pasar como parámetros. Los delegados son análogos a los tipos de función proporcionados por
los lenguajes funcionales. También son similares al concepto de punteros de función de otros lenguajes. A
diferencia de los punteros de función, los delegados están orientados a objetos y tienen seguridad de tipos.
Los tipos class , struct , interface y delegate admiten parámetros genéricos, mediante los que se pueden
parametrizar con otros tipos.
C# admite matrices unidimensionales y multidimensionales de cualquier tipo. A diferencia de los tipos
enumerados antes, no es necesario declarar los tipos de matriz antes de usarlos. En su lugar, los tipos de matriz
se crean mediante un nombre de tipo entre corchetes. Por ejemplo, int[] es una matriz unidimensional de
int , int[,] es una matriz bidimensional de int y int[][] es una matriz unidimensional de las matrices
unidimensionales, o la matriz "escalonada", de int .
Los tipos que aceptan valores NULL no requieren una definición independiente. Para cada tipo T que no acepta
valores NULL, existe un tipo T? que acepta valores NULL correspondiente, que puede tener un valor adicional,
null . Por ejemplo, int? es un tipo que puede contener cualquier entero de 32 bits o el valor null y string?
es un tipo que puede contener cualquier string o el valor null .
El sistema de tipos de C# está unificado, de tal forma que un valor de cualquier tipo puede tratarse como
object . Todos los tipos de C# directa o indirectamente se derivan del tipo de clase object , y object es la clase
base definitiva de todos los tipos. Los valores de tipos de referencia se tratan como objetos mediante la
visualización de los valores como tipo object . Los valores de tipos de valor se tratan como objetos mediante la
realización de operaciones de conversión boxing y operaciones de conversión unboxing. En el ejemplo siguiente,
un valor int se convierte en object y vuelve a int .
int i = 123;
object o = i; // Boxing
int j = (int)o; // Unboxing
Cuando se asigna un valor de un tipo de valor a una referencia object , se asigna un "box" para contener el
valor. Ese box es una instancia de un tipo de referencia, y es donde se copia el valor. Por el contrario, cuando una
referencia object se convierte en un tipo de valor, se comprueba si el elemento object al que se hace
referencia es un box del tipo de valor correcto. Si la comprobación se realiza correctamente, el valor del box se
copia en el tipo de valor.
El sistema de tipos unificado de C# conlleva efectivamente que los tipos de valor se tratan como referencias
object "a petición". Debido a la unificación, las bibliotecas de uso general que utilizan el tipo object pueden
usarse con todos los tipos que se derivan de object , como, por ejemplo, los tipos de referencia y los tipos de
valor.
Hay varios tipos de variables en C#, entre otras, campos, elementos de matriz, variables locales y parámetros.
Las variables representan ubicaciones de almacenamiento. Cada variable tiene un tipo que determina qué
valores se pueden almacenar en ella, como se muestra a continuación.
Tipo de valor distinto a NULL
Un valor de ese tipo exacto
Tipos de valor NULL
Un valor null o un valor de ese tipo exacto
objeto
Una referencia null , una referencia a un objeto de cualquier tipo de referencia o una referencia a un
valor de conversión boxing de cualquier tipo de valor
Tipo de clase
Una referencia null , una referencia a una instancia de ese tipo de clase o una referencia a una
instancia de una clase derivada de ese tipo de clase
Tipo de interfaz
Un referencia null , una referencia a una instancia de un tipo de clase que implementa dicho tipo de
interfaz o una referencia a un valor de conversión boxing de un tipo de valor que implementa dicho
tipo de interfaz
Tipo de matriz
Una referencia null , una referencia a una instancia de ese tipo de matriz o una referencia a una
instancia de un tipo de matriz compatible
Tipo delegado
Una referencia null o una referencia a una instancia de un tipo delegado compatible
namespace Acme.Collections
{
public class Stack<T>
{
Entry _top;
public T Pop()
{
if (_top == null)
{
throw new InvalidOperationException();
}
T result = _top.Data;
_top = _top.Next;
return result;
}
class Entry
{
public Entry Next { get; set; }
public T Data { get; set; }
El nombre completo de esta clase es Acme.Collections.Stack . La clase contiene varios miembros: un campo
denominado top , dos métodos denominados Push y Pop , y una clase anidada denominada Entry . La clase
Entry contiene además tres miembros: un campo denominado next , un campo denominado data y un
constructor. Stack es una clase genérica. Tiene un parámetro de tipo, T , que se reemplaza con un tipo
concreto cuando se usa.
Una pila es una colección de tipo "el primero que entra es el último que sale" (FILO). Los elementos nuevos se
agregan a la parte superior de la pila. Cuando se quita un elemento, se quita de la parte superior de la pila. En el
ejemplo anterior se declara el tipo Stack que define el almacenamiento y comportamiento de una pila. Puede
declarar una variable que haga referencia a una instancia del tipo Stack para usar esa funcionalidad.
Los ensamblados contienen código ejecutable en forma de instrucciones de lenguaje intermedio (IL) e
información simbólica en forma de metadatos. Antes de ejecutarlo, el compilador Just-In-Time (JIT) del entorno
de ejecución de .NET convierte el código de IL de un ensamblado en código específico del procesador.
Como un ensamblado es una unidad autodescriptiva de funcionalidad que contiene código y metadatos, no hay
necesidad de directivas #include ni archivos de encabezado de C#. Los tipos y miembros públicos contenidos
en un ensamblado determinado estarán disponibles en un programa de C# simplemente haciendo referencia a
dicho ensamblado al compilar el programa. Por ejemplo, este programa usa la clase Acme.Collections.Stack
desde el ensamblado acme.dll :
using System;
using Acme.Collections;
class Example
{
public static void Main()
{
var s = new Stack<int>();
s.Push(1); // stack contains 1
s.Push(10); // stack contains 1, 10
s.Push(100); // stack contains 1, 10, 100
Console.WriteLine(s.Pop()); // stack contains 1, 10
Console.WriteLine(s.Pop()); // stack contains 1
Console.WriteLine(s.Pop()); // stack is empty
}
}
Para compilar este programa, necesitaría hacer referencia al ensamblado que contiene la clase de pila que se
define en el ejemplo anterior.
Los programas de C# se pueden almacenar en varios archivos de origen. Cuando se compila un programa de
C#, todos los archivos de origen se procesan juntos y se pueden hacer referencia entre sí de manera libre.
Conceptualmente, es como si todos los archivos de origen estuviesen concatenados en un archivo de gran
tamaño antes de que se procesen. En C# nunca se necesitan declaraciones adelantadas porque, excepto en
contadas ocasiones, el orden de declaración es insignificante. C# no limita un archivo de origen a declarar
solamente un tipo público ni precisa que el nombre del archivo de origen coincida con un tipo declarado en el
archivo de origen.
En otros artículos de este paseo se explican estos bloques organizativos.
S IG U IE N TE
Tipos y miembros
16/09/2021 • 7 minutes to read
En cuanto lenguaje orientado a objetos, C# admite los conceptos de encapsulación, herencia y polimorfismo.
Una clase puede heredar directamente de una clase primaria e implementar cualquier número de interfaces. Los
métodos que invalidan los métodos virtuales en una clase primaria requieren la palabra clave override como
una manera de evitar redefiniciones accidentales. En C#, un struct es como una clase ligera; es un tipo asignado
en la pila que puede implementar interfaces pero que no admite la herencia. C# también proporciona registros,
que son tipos de clase cuyo propósito es, principalmente, almacenar valores de datos.
Clases y objetos
Las clases son los tipos más fundamentales de C#. Una clase es una estructura de datos que combina estados
(campos) y acciones (métodos y otros miembros de función) en una sola unidad. Una clase proporciona una
definición para instancias de la clase, también conocidas como objetos. Las clases admiten herencia y
polimorfismo, mecanismos por los que las clases derivadas pueden extender y especializar clases base.
Las clases nuevas se crean mediante declaraciones de clase. Una declaración de clase comienza con un
encabezado. El encabezado especifica lo siguiente:
Atributos y modificadores de la clase
Nombre de la clase
Clase base (al heredar de una clase base)
Interfaces implementadas por la clase
Al encabezado le sigue el cuerpo de la clase, que consta de una lista de declaraciones de miembros escritas
entre los delimitadores { y } .
En el código siguiente se muestra una declaración de una clase simple denominada Point :
Las instancias de clases se crean mediante el operador new , que asigna memoria para una nueva instancia,
invoca un constructor para inicializar la instancia y devuelve una referencia a la instancia. Las instrucciones
siguientes crean dos objetos Point y almacenan las referencias en esos objetos en dos variables:
La memoria ocupada por un objeto se reclama automáticamente cuando el objeto ya no es accesible. En C#, no
es necesario ni posible desasignar objetos de forma explícita.
Parámetros de tipo
Las clases genéricas definen parámetros de tipo . Los parámetros de tipo son una lista de nombres de
parámetros de tipo entre paréntesis angulares. Los parámetros de tipo siguen el nombre de la clase. Los
parámetros de tipo pueden usarse luego en el cuerpo de las declaraciones de clase para definir a los miembros
de la clase. En el ejemplo siguiente, los parámetros de tipo de Pair son TFirst y TSecond :
Un tipo de clase que se declara para tomar parámetros de tipo se conoce como tipo de clase genérica. Los tipos
de estructura, interfaz y delegado también pueden ser genéricos. Cuando se usa la clase genérica, se deben
proporcionar argumentos de tipo para cada uno de los parámetros de tipo:
Un tipo genérico con argumentos de tipo proporcionado, como Pair<int,string> anteriormente, se conoce
como tipo construido.
Clases base
Una declaración de clase puede especificar una clase base. Tras el nombre de clase y los parámetros de tipo,
agregue un signo de dos puntos y el nombre de la clase base. Omitir una especificación de la clase base es igual
que derivarla del tipo object . En el ejemplo siguiente, la clase base de Point3D es Point . En el primer ejemplo,
la clase base de Point es object :
Una clase hereda a los miembros de su clase base. La herencia significa que una clase contiene implícitamente
casi todos los miembros de su clase base. Una clase no hereda la instancia, los constructores estáticos ni el
finalizador. Una clase derivada puede agregar nuevos miembros a aquellos de los que hereda, pero no puede
quitar la definición de un miembro heredado. En el ejemplo anterior, Point3D hereda los miembros X y Y de
Point , y cada instancia de Point3D contiene tres miembros: X , Y y Z .
Existe una conversión implícita de un tipo de clase a cualquiera de sus tipos de clase base. Una variable de un
tipo de clase puede hacer referencia a una instancia de esa clase o a una instancia de cualquier clase derivada.
Por ejemplo, dadas las declaraciones de clase anteriores, una variable de tipo Point puede hacer referencia a
una instancia de Point o Point3D :
Estructuras
Las clases definen tipos que admiten la herencia y el polimorfismo. Permiten crear comportamientos
sofisticados basados en jerarquías de clases derivadas. Por el contrario, los tipos struct son tipos más sencillos
cuyo propósito principal es almacenar valores de datos. Dichos tipos struct no pueden declarar un tipo base; se
derivan implícitamente de System.ValueType. No se pueden derivar otros tipos de struct a partir de un tipo de
struct . Están sellados implícitamente.
Interfaces
Una *interfaz _ define un contrato que se puede implementar mediante clases y estructuras. Una _interfaz* se
define para declarar funcionalidades que se comparten entre distintos tipos. Por ejemplo, la interfaz
System.Collections.Generic.IEnumerable<T> define una manera coherente de recorrer todos los elementos de
una colección, como una matriz. Una interfaz puede contener métodos, propiedades, eventos e indexadores.
Normalmente, una interfaz no proporciona implementaciones de los miembros que define, sino que
simplemente especifica los miembros que se deben proporcionar mediante clases o estructuras que
implementan la interfaz.
Las interfaces pueden usar herencia múltiple . En el ejemplo siguiente, la interfaz IComboBox hereda de
ITextBox y IListBox .
interface IControl
{
void Paint();
}
Las clases y los structs pueden implementar varias interfaces. En el ejemplo siguiente, la clase EditBox
implementa IControl y IDataBound .
interface IDataBound
{
void Bind(Binder b);
}
Enumeraciones
Un tipo de enumeración define un conjunto de valores constantes. En el elemento enum siguiente se declaran
constantes que definen diferentes verduras de raíz:
También puede definir un elemento enum que se usará de forma combinada como marcas. La declaración
siguiente declara un conjunto de marcas para las cuatro estaciones. Se puede aplicar cualquier combinación de
estaciones, incluido un valor All que incluya todas las estaciones:
[Flags]
public enum Seasons
{
None = 0,
Summer = 1,
Autumn = 2,
Winter = 4,
Spring = 8,
All = Summer | Autumn | Winter | Spring
}
Tuplas
C# admite tuplas , lo cual proporciona una sintaxis concisa para agrupar varios elementos de datos en una
estructura de datos ligera. Puede crear una instancia de una tupla declarando los tipos y los nombres de los
miembros entre ( y ) , como se muestra en el ejemplo siguiente:
Las tuplas proporcionan una alternativa para la estructura de datos con varios miembros sin usar los bloques de
creación que se describen en el siguiente artículo.
A N TE R IO R S IG U IE N TE
Bloques de creación de programas
16/09/2021 • 26 minutes to read
Los tipos descritos en el artículo anterior se compilan con estos bloques de creación: *miembros _, expresiones
e _ *instrucciones**.
Miembros
Los miembros de class son estáticos _ o _de instancia**. Los miembros estáticos pertenecen a clases y los
miembros de instancia pertenecen a objetos (instancias de clases).
En la lista siguiente se proporciona una visión general de los tipos de miembros que puede contener una clase.
Constantes : Valores constantes asociados a la clase
Campos : variables que están asociadas a la clase.
Métodos : acciones que puede realizar la clase.
Propiedades : Acciones asociadas a la lectura y escritura de propiedades con nombre de la clase
Indizadores : Acciones asociadas a la indexación de instancias de la clase como una matriz
Eventos : Notificaciones que puede generar la clase
Operadores : Conversiones y operadores de expresión admitidos por la clase
Constructores : Acciones necesarias para inicializar instancias de la clase o la clase propiamente dicha
Finalizadores : acciones que se realizan antes de que las instancias de la clase se descarten de forma
permanente
Tipos : Tipos anidados declarados por la clase
Accesibilidad
Cada miembro de una clase tiene asociada una accesibilidad, que controla las regiones del texto del programa
que pueden acceder al miembro. Existen seis formas de accesibilidad posibles. A continuación se resumen los
modificadores de acceso.
public : El acceso no está limitado.
private : El acceso está limitado a esta clase.
protected : El acceso está limitado a esta clase o a las clases derivadas de esta clase.
internal : El acceso está limitado al ensamblado actual ( .exe o .dll ).
protected internal : El acceso está limitado a esta clase, las clases derivadas de la misma o las clases que
forman parte del mismo ensamblado.
private protected : El acceso está limitado a esta clase o a las clases derivadas de este tipo que forman parte
del mismo ensamblado.
Campos
Un campo es una variable que está asociada con una clase o a una instancia de una clase.
Un campo declarado con el modificador "static" define un campo estático. Un campo estático identifica
exactamente una ubicación de almacenamiento. Independientemente del número de instancias de una clase que
se creen, solo hay una única copia de un campo estático.
Un campo declarado sin el modificador "static" define un campo de instancia. Cada instancia de una clase
contiene una copia independiente de todos los campos de instancia de esa clase.
En el ejemplo siguiente, cada instancia de la clase Color tiene una copia independiente de los campos de
instancia R , G y B , pero solo hay una copia de los campos estáticos Black , White , Red , Green y Blue :
public byte R;
public byte G;
public byte B;
Como se muestra en el ejemplo anterior, los campos de solo lectura se puede declarar con un modificador
readonly . La asignación a un campo de solo lectura únicamente se puede producir como parte de la
declaración del campo o en un constructor de la misma clase.
Métodos
Un método es un miembro que implementa un cálculo o una acción que puede realizar un objeto o una clase. A
los métodos estáticos se accede a través de la clase. A los métodos de instancia se accede a través de instancias
de la clase.
Los métodos pueden tener una lista de parámetros, los cuales representan valores o referencias a variables que
se pasan al método. Los métodos tienen un tipo de valor devuelto, el cual especifica el tipo del valor calculado y
devuelto por el método. El tipo de valor devuelto de un método es void si no devuelve un valor.
Al igual que los tipos, los métodos también pueden tener un conjunto de parámetros de tipo, para lo cuales se
deben especificar argumentos de tipo cuando se llama al método. A diferencia de los tipos, los argumentos de
tipo a menudo se pueden deducir de los argumentos de una llamada al método y no es necesario
proporcionarlos explícitamente.
La signatura de un método debe ser única en la clase en la que se declara el método. La signatura de un método
se compone del nombre del método, el número de parámetros de tipo y el número, los modificadores y los
tipos de sus parámetros. La signatura de un método no incluye el tipo de valor devuelto.
Cuando el cuerpo del método es una expresión única, el método se puede definir con un formato de expresión
compacta, tal y como se muestra en el ejemplo siguiente:
Parámetros
Los parámetros se usan para pasar valores o referencias a variables a métodos. Los parámetros de un método
obtienen sus valores reales de los argumentos que se especifican cuando se invoca el método. Hay cuatro tipos
de parámetros: parámetros de valor, parámetros de referencia, parámetros de salida y matrices de parámetros.
Un parámetro de valor se usa para pasar argumentos de entrada. Un parámetro de valor corresponde a una
variable local que obtiene su valor inicial del argumento que se ha pasado para el parámetro. Las
modificaciones de un parámetro de valor no afectan el argumento que se ha pasado para el parámetro.
Los parámetros de valor pueden ser opcionales; se especifica un valor predeterminado para que se puedan
omitir los argumentos correspondientes.
Un parámetro de referencia se usa para pasar argumentos mediante una referencia. El argumento pasado para
un parámetro de referencia debe ser una variable con un valor definido. Durante la ejecución del método, el
parámetro de referencia representa la misma ubicación de almacenamiento que la variable del argumento. Un
parámetro de referencia se declara con el modificador ref . En el ejemplo siguiente se muestra el uso de
parámetros ref .
Un parámetro de salida se usa para pasar argumentos mediante una referencia. Es similar a un parámetro de
referencia, excepto que no necesita que asigne un valor explícitamente al argumento proporcionado por el autor
de la llamada. Un parámetro de salida se declara con el modificador out . En el siguiente ejemplo se muestra el
uso de los parámetros out con la sintaxis que se ha presentado en C# 7.
static void Divide(int x, int y, out int result, out int remainder)
{
result = x / y;
remainder = x % y;
}
Una matriz de parámetros permite que se pasen a un método un número variable de argumentos. Una matriz
de parámetros se declara con el modificador params . Solo el último parámetro de un método puede ser una
matriz de parámetros y el tipo de una matriz de parámetros debe ser un tipo de matriz unidimensional. Los
métodos Write y WriteLine de la clase System.Console son buenos ejemplos de uso de la matriz de
parámetros. Se declaran de la manera siguiente.
Dentro de un método que usa una matriz de parámetros, la matriz de parámetros se comporta exactamente
igual que un parámetro normal de un tipo de matriz. Pero en una invocación de un método con una matriz de
parámetros, es posible pasar un único argumento del tipo de matriz de parámetros o cualquier número de
argumentos del tipo de elemento de la matriz de parámetros. En este caso, una instancia de matriz se e inicializa
automáticamente con los argumentos dados. Este ejemplo
int x, y, z;
x = 3;
y = 4;
z = 5;
Console.WriteLine("x={0} y={1} z={2}", x, y, z);
int x = 3, y = 4, z = 5;
class Squares
{
public static void WriteSquares()
{
int i = 0;
int j;
while (i < 10)
{
j = i * i;
Console.WriteLine($"{i} x {i} = {j}");
i = i + 1;
}
}
}
C# requiere que se asigne definitivamente una variable local antes de que se pueda obtener su valor. Por
ejemplo, si la declaración de i anterior no incluyera un valor inicial, el compilador notificaría un error con los
usos posteriores de i porque i no se asignaría definitivamente en esos puntos del programa.
Puede usar una instrucción return para devolver el control a su llamador. En un método que devuelve void ,
las instrucciones return no pueden especificar una expresión. En un método que devuelve valores distintos de
void, las instrucciones return deben incluir una expresión que calcula el valor devuelto.
Métodos estáticos y de instancia
Un método declarado con un modificador static es un método estático. Un método estático no opera en una
instancia específica y solo puede acceder directamente a miembros estáticos.
Un método declarado sin un modificador static es un método de instancia. Un método de instancia opera en
una instancia específica y puede acceder a miembros estáticos y de instancia. Se puede acceder explícitamente a
la instancia en la que se invoca un método de instancia como this . Es un error hacer referencia a this en un
método estático.
La siguiente clase Entity tiene miembros estáticos y de instancia.
class Entity
{
static int s_nextSerialNo;
int _serialNo;
public Entity()
{
_serialNo = s_nextSerialNo++;
}
Cada instancia de Entity contiene un número de serie (y, probablemente, otra información que no se muestra
aquí). El constructor Entity (que es como un método de instancia) inicializa la nueva instancia con el siguiente
número de serie disponible. Como el constructor es un miembro de instancia, se le permite acceder al campo de
instancia _serialNo y al campo estático s_nextSerialNo .
Los métodos estáticos GetNextSerialNo y SetNextSerialNo pueden acceder al campo estático s_nextSerialNo ,
pero sería un error para ellas acceder directamente al campo de instancia _serialNo .
En el ejemplo siguiente se muestra el uso de la clase Entity .
Entity.SetNextSerialNo(1000);
Entity e1 = new Entity();
Entity e2 = new Entity();
Console.WriteLine(e1.GetSerialNo()); // Outputs "1000"
Console.WriteLine(e2.GetSerialNo()); // Outputs "1001"
Console.WriteLine(Entity.GetNextSerialNo()); // Outputs "1002"
Los métodos estáticos SetNextSerialNo y GetNextSerialNo se invocan en la clase, mientras que el método de
instancia GetSerialNo se invoca en instancias de la clase.
Métodos virtual, de reemplazo y abstracto
Use métodos virtuales, de invalidación y abstractos para definir el comportamiento de una jerarquía de tipos de
clase. Como una clase se puede derivar de una clase base, es posible que tenga que modificar el
comportamiento implementado en la clase base de esas clases derivadas. Un método vir tual _ es el que se
declara e implementa en una clase base donde cualquier clase derivada puede proporcionar una
implementación más específica. Un método de _reemplazo_ es el que se implementa en una clase derivada que
modifica el comportamiento de la implementación de la clase base. Un método _abstracto_ es el que se declara
en una clase base que se _debe reemplazar en todas las clases derivadas. De hecho, los métodos abstractos no
definen una implementación en la clase base.
Las llamadas de métodos de instancia se pueden resolver en implementaciones de la clase base o de clases
derivadas. El tipo de una variable determina su tipo en tiempo de compilación. El tipo en tiempo de compilación
es el que usa el compilador para determinar sus miembros. Pero una variable se puede asignar a una instancia
de cualquier tipo derivado de su tipo en tiempo de compilación. El tipo en tiempo de ejecución es el tipo de la
instancia a la que hace referencia esa variable.
Cuando se invoca un método virtual, el tipo en tiempo de ejecución de la instancia para la que tiene lugar esa
invocación determina la implementación del método real que se invocará. En una invocación de método no
virtual, el tipo en tiempo de compilación de la instancia es el factor determinante.
Un método virtual puede ser reemplazado en una clase derivada. Cuando una declaración de método de
instancia incluye un modificador "override", el método reemplaza un método virtual heredado con la misma
signatura. Una declaración de método virtual presenta un nuevo método. Una declaración de método de
reemplazo especializa un método virtual heredado existente proporcionando una nueva implementación de ese
método.
Un método abstracto es un método virtual sin implementación. Un método abstracto se declara con el
modificador abstract y solo se permite en una clase abstracta. Un método abstracto debe reemplazarse en
todas las clases derivadas no abstractas.
En el ejemplo siguiente se declara una clase abstracta, Expression , que representa un nodo de árbol de
expresión y tres clases derivadas, Constant , VariableReference y Operation , que implementan nodos de árbol
de expresión para constantes, referencias a variables y operaciones aritméticas. Este ejemplo es similar a los
tipos de árbol de expresión, pero no está relacionada con ellos.
public abstract class Expression
{
public abstract double Evaluate(Dictionary<string, object> vars);
}
Las cuatro clases anteriores se pueden usar para modelar expresiones aritméticas. Por ejemplo, usando
instancias de estas clases, la expresión x + 3 se puede representar de la manera siguiente.
El método Evaluate de una instancia Expression se invoca para evaluar la expresión determinada y generar un
valor double . El método toma un argumento Dictionary que contiene nombres de variables (como claves de
las entradas) y valores (como valores de las entradas). Como Evaluate es un método abstracto, las clases no
abstractas que derivan de Expression deben invalidar Evaluate .
Una implementación de Constant de Evaluate simplemente devuelve la constante almacenada. Una
implementación de VariableReference busca el nombre de variable en el diccionario y devuelve el valor
resultante. Una implementación de Operation evalúa primero los operandos izquierdo y derecho (mediante la
invocación recursiva de sus métodos Evaluate ) y luego realiza la operación aritmética correspondiente.
El siguiente programa usa las clases Expression para evaluar la expresión x * (y + 2) para los distintos
valores de x y y .
Sobrecarga de métodos
La sobrecarga de métodos permite que varios métodos de la misma clase tengan el mismo nombre mientras
tengan signaturas únicas. Al compilar una invocación de un método sobrecargado, el compilador usa la
resolución de sobrecarga para determinar el método concreto que de invocará. La resolución de sobrecarga
busca el método que mejor coincida con los argumentos. Si no se puede encontrar la mejor coincidencia, se
genera un error. En el ejemplo siguiente se muestra la resolución de sobrecarga en vigor. El comentario para
cada invocación del método UsageExample muestra qué método se invoca.
class OverloadingExample
{
static void F() => Console.WriteLine("F()");
static void F(object x) => Console.WriteLine("F(object)");
static void F(int x) => Console.WriteLine("F(int)");
static void F(double x) => Console.WriteLine("F(double)");
static void F<T>(T x) => Console.WriteLine("F<T>(T)");
static void F(double x, double y) => Console.WriteLine("F(double, double)");
Tal como se muestra en el ejemplo, un método determinado siempre se puede seleccionar mediante la
conversión explícita de los argumentos en los tipos de parámetros exactos o los argumentos de tipo.
T[] _items;
int _count;
Constructores
C# admite constructores de instancia y estáticos. Un constructor de instancia es un miembro que implementa
las acciones necesarias para inicializar una instancia de una clase. Un constructor estático es un miembro que
implementa las acciones necesarias para inicializar una clase en sí misma cuando se carga por primera vez.
Un constructor se declara como un método sin ningún tipo de valor devuelto y el mismo nombre que la clase
contenedora. Si una declaración de constructor incluye un modificador static , declara un constructor estático.
De lo contrario, declara un constructor de instancia.
Los constructores de instancia pueden sobrecargarse y tener parámetros opcionales. Por ejemplo, la clase
MyList<T> declara un constructor de instancia con único parámetro int opcional. Los constructores de
instancia se invocan mediante el operador new . Las siguientes instrucciones asignan dos instancias
MyList<string> mediante el constructor de la clase MyList con y sin el argumento opcional.
MyList<string> list1 = new MyList<string>();
MyList<string> list2 = new MyList<string>(10);
A diferencia de otros miembros, los constructores de instancias no se heredan. Una clase no tiene constructores
de instancia que no sean los que se declaren realmente en la misma. Si no se proporciona ningún constructor de
instancia para una clase, se proporciona automáticamente uno vacío sin ningún parámetro.
Propiedades
Las propiedades son una extensión natural de los campos. Ambos son miembros con nombre con tipos
asociados y la sintaxis para acceder a los campos y las propiedades es la misma. Pero a diferencia de los
campos, las propiedades no denotan ubicaciones de almacenamiento. Las propiedades tienen descriptores de
acceso que especifican las instrucciones ejecutadas cuando se leen o escriben sus valores. Un descriptor de
acceso get lee el valor. Un descriptor de acceso set escribe el valor.
Una propiedad se declara como un campo, salvo que la declaración finaliza con un descriptor de acceso get o un
descriptor de acceso set escrito entre los delimitadores { y } en lugar de finalizar en un punto y coma. Una
propiedad que tiene un descriptor de acceso get y un descriptor de acceso set es una propiedad de lectura y
escritura. Una propiedad que solo tiene un descriptor de acceso get es una propiedad de solo lectura. Una
propiedad que solo tiene un descriptor de acceso set es una propiedad de solo escritura.
Un descriptor de acceso get corresponde a un método sin parámetros con un valor devuelto del tipo de
propiedad. Un descriptor de acceso set corresponde a un método con un solo parámetro denominado value y
ningún tipo de valor devuelto. El descriptor de acceso get calcula el valor de la propiedad. El descriptor de
acceso set proporciona un nuevo valor para la propiedad. Cuando la propiedad es el destino de una asignación,
o el operando de ++ o -- , se invoca al descriptor de acceso set. En otros casos en los que se hace referencia a
la propiedad, se invoca al descriptor de acceso get.
La clase MyList<T> declara dos propiedades, Count y Capacity , que son de solo lectura y de lectura y escritura,
respectivamente. El código siguiente es un ejemplo de uso de estas propiedades:
De forma similar a los campos y métodos, C# admite propiedades de instancia y propiedades estáticas. Las
propiedades estáticas se declaran con el modificador "static", y las propiedades de instancia se declaran sin él.
Los descriptores de acceso de una propiedad pueden ser virtuales. Cuando una declaración de propiedad
incluye un modificador virtual , abstract o override , se aplica a los descriptores de acceso de la propiedad.
Indexadores
Un indexador es un miembro que permite indexar de la misma manera que una matriz. Un indexador se declara
como una propiedad, excepto por el hecho que el nombre del miembro es this , seguido por una lista de
parámetros que se escriben entre los delimitadores [ y ] . Los parámetros están disponibles en los
descriptores de acceso del indexador. De forma similar a las propiedades, los indexadores pueden ser lectura y
escritura, de solo lectura y de solo escritura, y los descriptores de acceso de un indexador pueden ser virtuales.
La clase MyList<T> declara un único indexador de lectura y escritura que toma un parámetro int . El indexador
permite indexar instancias de MyList<T> con valores int . Por ejemplo:
MyList<string> names = new MyList<string>();
names.Add("Liz");
names.Add("Martha");
names.Add("Beth");
for (int i = 0; i < names.Count; i++)
{
string s = names[i];
names[i] = s.ToUpper();
}
Los indizadores se pueden sobrecargar. Una clase puede declarar varios indexadores siempre y cuando el
número o los tipos de sus parámetros sean diferentes.
Eventos
Un evento es un miembro que permite que una clase u objeto proporcionen notificaciones. Un evento se declara
como un campo, excepto por el hecho de que la declaración incluye una palabra clave event , y el tipo debe ser
un tipo delegado.
Dentro de una clase que declara un miembro de evento, el evento se comporta como un campo de un tipo
delegado (siempre que el evento no sea abstracto y no declare descriptores de acceso). El campo almacena una
referencia a un delegado que representa los controladores de eventos que se han agregado al evento. Si no
existen controladores de eventos, el campo es null .
La clase MyList<T> declara un único miembro de evento llamado Changed , lo que indica que se ha agregado un
nuevo elemento a la lista. El método virtual OnChanged genera el evento cambiado, y comprueba primero si el
evento es null (lo que significa que no existen controladores). La noción de generar un evento es equivalente
exactamente a invocar el delegado representado por el evento. No hay ninguna construcción especial de
lenguaje para generar eventos.
Los clientes reaccionan a los eventos mediante controladores de eventos. Los controladores de eventos se
asocian mediante el operador += y se quitan con el operador -= . En el ejemplo siguiente se asocia un
controlador de eventos con el evento Changed de un objeto MyList<string> .
class EventExample
{
static int s_changeCount;
Para escenarios avanzados donde se quiere controlar el almacenamiento subyacente de un evento, una
declaración de evento puede proporcionar de forma explícita los descriptores de acceso add y remove , que son
similares al descriptor de acceso set de una propiedad.
Operadores
Un operador es un miembro que define el significado de aplicar un operador de expresión determinado a las
instancias de una clase. Se pueden definir tres tipos de operadores: operadores unarios, operadores binarios y
operadores de conversión. Todos los operadores se deben declarar como public y static .
La clase MyList<T> declara dos operadores, operator == y operator != . Los operadores de reemplazo
proporcionan un nuevo significado a expresiones que aplican esos operadores a instancias MyList . En concreto,
los operadores definen la igualdad de dos instancias MyList<T> como la comparación de cada uno de los
objetos contenidos con sus métodos Equals . En el ejemplo siguiente se usa el operador == para comparar dos
instancias MyList<int> .
El primer objeto Console.WriteLine genera True porque las dos listas contienen el mismo número de objetos
con los mismos valores en el mismo orden. Si MyList<T> no hubiera definido operator == , el primer objeto
Console.WriteLine habría generado False porque a y b hacen referencia a diferentes instancias de
MyList<int> .
Finalizadores
Un finalizador es un miembro que implementa las acciones necesarias para finalizar una instancia de una clase.
Normalmente, se necesita un finalizador para liberar los recursos no administrados. Los finalizadores no pueden
tener parámetros, no pueden tener modificadores de accesibilidad y no se pueden invocar de forma explícita. El
finalizador de una instancia se invoca automáticamente durante la recolección de elementos no utilizados. Para
obtener más información, vea el artículo sobre finalizadores.
El recolector de elementos no utilizados tiene una amplia libertad para decidir cuándo debe recolectar objetos y
ejecutar finalizadores. En concreto, los intervalos de las invocaciones de finalizador no son deterministas y los
finalizadores se pueden ejecutar en cualquier subproceso. Por estas y otras razones, las clases deben
implementar finalizadores solo cuando no haya otras soluciones que sean factibles.
La instrucción using proporciona un mejor enfoque para la destrucción de objetos.
Expresiones
Las expresiones se construyen con operandos y operadores. Los operadores de una expresión indican qué
operaciones se aplican a los operandos. Ejemplos de operadores incluyen + , - , * , / y new . Algunos
ejemplos de operandos son literales, campos, variables locales y expresiones.
Cuando una expresión contiene varios operadores, su precedencia controla el orden en el que se evalúan los
operadores individuales. Por ejemplo, la expresión x + y * z se evalúa como x + (y * z) porque el operador
* tiene mayor precedencia que el operador + .
Cuando un operando se encuentra entre dos operadores con la misma precedencia, la asociatividad de los
operadores controla el orden en que se realizan las operaciones:
Excepto los operadores de asignación y los operadores de fusión de NULL, todos los operadores binarios son
asociativos a la izquierda, lo que significa que las operaciones se realizan de izquierda a derecha. Por
ejemplo, x + y + z se evalúa como (x + y) + z .
Los operadores de asignación, los operadores de fusión de NULL ?? y ??= y el operador condicional ?:
son asociativos a la derecha, lo que significa que las operaciones se realizan de derecha a izquierda. Por
ejemplo, x = y = z se evalúa como x = (y = z) .
Instrucciones
Las acciones de un programa se expresan mediante instrucciones. C# admite varios tipos de instrucciones
diferentes, varias de las cuales se definen en términos de instrucciones insertadas.
Un bloque permite que se escriban varias instrucciones en contextos donde se permite una única instrucción.
Un bloque se compone de una lista de instrucciones escritas entre los delimitadores { y } .
Las instrucciones de declaración se usan para declarar variables locales y constantes.
Las instrucciones de expresión se usan para evaluar expresiones. Las expresiones que pueden usarse como
instrucciones incluyen invocaciones de método, asignaciones de objetos mediante el operador new ,
asignaciones mediante = y los operadores de asignación compuestos, operaciones de incremento y
decremento mediante los operadores ++ y -- y expresiones await .
Las instrucciones de selección se usan para seleccionar una de varias instrucciones posibles para su
ejecución en función del valor de alguna expresión. Este grupo contiene las instrucciones if y switch .
Las instrucciones de iteración se usan para ejecutar una instrucción insertada de forma repetida. Este grupo
contiene las instrucciones while , do , for y foreach .
Las instrucciones de salto se usan para transferir el control. Este grupo contiene las instrucciones break ,
continue , goto , throw , return y yield .
La instrucción try ... catch se usa para detectar excepciones que se producen durante la ejecución de un
bloque, y la instrucción try ... finally se usa para especificar el código de finalización que siempre se
ejecuta, tanto si se ha producido una excepción como si no.
Las instrucciones checked y unchecked sirven para controlar el contexto de comprobación de
desbordamiento para conversiones y operaciones aritméticas de tipo integral.
La instrucción lock se usa para obtener el bloqueo de exclusión mutua para un objeto determinado,
ejecutar una instrucción y, luego, liberar el bloqueo.
La instrucción using se usa para obtener un recurso, ejecutar una instrucción y, luego, eliminar dicho
recurso.
A continuación se enumeran los tipos de instrucciones que se pueden usar:
Declaración de variable local
Declaración de constante local
Instrucción de expresión
Instrucción if
Instrucción switch
Instrucción while
Instrucción do
Instrucción for
Instrucción foreach
Instrucción break
Instrucción continue
Instrucción goto
Instrucción return
Instrucción yield
Instrucciones throw y try
Instrucciones checked y unchecked
Instrucción lock
Instrucción using
A N TE R IO R S IG U IE N TE
Áreas principales del lenguaje
16/09/2021 • 10 minutes to read
Este ejemplo crea una matriz unidimensional _ y opera en ella. C# también admite _matrices
multidimensionales_. El número de dimensiones de un tipo de matriz, conocido también como _ clasificación del
tipo de matriz, es uno más el número de comas entre los corchetes del tipo de matriz. En el ejemplo siguiente se
asignan una matriz unidimensional, bidimensional y tridimensional, respectivamente.
La primera línea crea una matriz con tres elementos, cada uno de tipo int[] y cada uno con un valor inicial de
null . Las líneas siguientes inicializan entonces los tres elementos con referencias a instancias de matriz
individuales de longitud variable.
El operador new permite especificar los valores iniciales de los elementos de matriz mediante un inicializador
de matriz , que es una lista de las expresiones escritas entre los delimitadores { y } . En el ejemplo siguiente
se asigna e inicializa un tipo int[] con tres elementos.
int[] a = { 1, 2, 3 };
La instrucción foreach se puede utilizar para enumerar los elementos de cualquier colección. El código
siguiente numera la matriz del ejemplo anterior:
La instrucción foreach utiliza la interfaz IEnumerable<T>, por lo que puede trabajar con cualquier colección.
Interpolación de cadenas
La interpolación de cadenas de C# le permite dar formato a las cadenas mediante la definición de
expresiones cuyos resultados se colocan en una cadena de formato. Por ejemplo, en el ejemplo siguiente se
imprime la temperatura de un día determinado a partir de un conjunto de datos meteorológicos:
Detección de patrones
El lenguaje C# proporciona expresiones de coincidencia de patrones para consultar el estado de un objeto y
ejecutar código basado en dicho estado. Puede inspeccionar los tipos y los valores de las propiedades y los
campos para determinar qué acción se debe realizar. La expresión switch es la expresión primaria para la
coincidencia de patrones.
class Multiplier
{
double _factor;
class DelegateExample
{
static double[] Apply(double[] a, Function f)
{
var result = new double[a.Length];
for (int i = 0; i < a.Length; i++) result[i] = f(a[i]);
return result;
}
Una instancia del tipo de delegado Function puede hacer referencia a cualquier método que tome un
argumento double y devuelva un valor double . El método Apply aplica un elemento Function determinado a
los elementos de double[] y devuelve double[] con los resultados. En el método Main , Apply se usa para
aplicar tres funciones diferentes a un valor double[] .
Un delegado puede hacer referencia a un método estático (como Square o Math.Sin en el ejemplo anterior) o
un método de instancia (como m.Multiply en el ejemplo anterior). Un delegado que hace referencia a un
método de instancia también hace referencia a un objeto determinado y, cuando se invoca el método de
instancia a través del delegado, ese objeto se convierte en this en la invocación.
Los delegados también se pueden crear mediante funciones anónimas, que son "métodos insertados" que se
crean al declararlos. Las funciones anónimas pueden ver las variables locales de los métodos adyacentes. En el
ejemplo siguiente no se crea una clase:
Un delegado no conoce la clase del método al que hace referencia; de hecho, tampoco tiene importancia. El
método al que se hace referencia debe tener los mismos parámetros y el mismo tipo de valor devuelto que el
delegado.
async y await
C# admite programas asincrónicos con dos palabras clave: async y await . Puede agregar el modificador
async a una declaración de método para declarar que dicho método es asincrónico. El operador await indica
al compilador que espere de forma asincrónica a que finalice un resultado. El control se devuelve al autor de la
llamada, y el método devuelve una estructura que administra el estado del trabajo asincrónico. Normalmente, la
estructura es un elemento System.Threading.Tasks.Task<TResult>, pero puede ser cualquier tipo que admita el
patrón awaiter. Estas características permiten escribir código que se lee como su homólogo sincrónico, pero que
se ejecuta de forma asincrónica. Por ejemplo, el código siguiente descarga la página principal de Microsoft Docs:
Atributos
Los tipos, los miembros y otras entidades en un programa de C # admiten modificadores que controlan ciertos
aspectos de su comportamiento. Por ejemplo, la accesibilidad de un método se controla mediante los
modificadores public , protected , internal y private . C # generaliza esta funcionalidad de manera que los
tipos de información declarativa definidos por el usuario se puedan adjuntar a las entidades del programa y
recuperarse en tiempo de ejecución. Los programas especifican esta información declarativa mediante la
definición y el uso de atributos .
En el ejemplo siguiente se declara un atributo HelpAttribute que se puede colocar en entidades de programa
para proporcionar vínculos a la documentación asociada.
public class HelpAttribute : Attribute
{
string _url;
string _topic;
Todas las clases de atributos se derivan de la clase base Attribute proporcionada por la biblioteca .NET. Los
atributos se pueden aplicar proporcionando su nombre, junto con cualquier argumento, entre corchetes, justo
antes de la declaración asociada. Si el nombre de un atributo termina en Attribute , esa parte del nombre se
puede omitir cuando se hace referencia al atributo. Por ejemplo, HelpAttribute se puede usar de la manera
siguiente.
[Help("https://docs.microsoft.com/dotnet/csharp/tour-of-csharp/features")]
public class Widget
{
[Help("https://docs.microsoft.com/dotnet/csharp/tour-of-csharp/features",
Topic = "Display")]
public void Display(string text) { }
}
En este ejemplo se adjunta un atributo HelpAttribute a la clase Widget . También se agrega otro atributo
HelpAttribute al método Display en la clase. Los constructores públicos de una clase de atributos controlan la
información que se debe proporcionar cuando el atributo se adjunta a una entidad de programa. Se puede
proporcionar información adicional haciendo referencia a las propiedades públicas de lectura y escritura de la
clase de atributos (como la referencia a la propiedad Topic usada anteriormente).
Los metadatos definidos por atributos pueden leerse y manipularse en tiempo de ejecución mediante reflexión.
Cuando se solicita un atributo determinado mediante esta técnica, se invoca el constructor de la clase de
atributos con la información proporcionada en el origen del programa. Se devuelve la instancia del atributo
resultante. Si se proporciona información adicional mediante propiedades, dichas propiedades se establecen en
los valores dados antes de devolver la instancia del atributo.
El siguiente ejemplo de código demuestra cómo obtener las HelpAttribute instancias asociadas a la clase
Widget y su método Display .
Type widgetType = typeof(Widget);
if (widgetClassAttributes.Length > 0)
{
HelpAttribute attr = (HelpAttribute)widgetClassAttributes[0];
Console.WriteLine($"Widget class help URL : {attr.Url} - Related topic : {attr.Topic}");
}
if (displayMethodAttributes.Length > 0)
{
HelpAttribute attr = (HelpAttribute)displayMethodAttributes[0];
Console.WriteLine($"Display method help URL : {attr.Url} - Related topic : {attr.Topic}");
}
Más información
Puede explorar más sobre C# con uno de nuestros tutoriales.
A N TE R IO R
Introducción a C#
16/09/2021 • 3 minutes to read
Le damos la bienvenida a los tutoriales de introducción a C#. Estas lecciones empiezan con código interactivo
que puede ejecutar en su explorador. Puede obtener información sobre los conceptos básicos de C# en la serie
de vídeos C# 101 antes de comenzar estas lecciones interactivas.
En las primeras lecciones se explican los conceptos de C# con la utilización de pequeños fragmentos de código.
Aprenderá los datos básicos de la sintaxis de C# y cómo trabajar con tipos de datos como cadenas, números y
booleanos. Se trata de material totalmente interactivo, que le permitirá empezar a escribir y ejecutar código en
cuestión de minutos. En las primeras lecciones se asume que no dispone de conocimientos previos sobre
programación o sobre el lenguaje C#.
Puede probar estos tutoriales en entornos diferentes. Los conceptos que aprenderá son los mismos. La
diferencia estará en el tipo de experiencia que elija:
En el explorador, en la plataforma de documentos: esta experiencia inserta una ventana de código de C#
ejecutable en las páginas de documentos. Deberá escribir y ejecutar el código de C# en el explorador.
En la experiencia de Microsoft Learn: esta ruta de aprendizaje contiene varios módulos en los que se exponen
los conceptos básicos de C#.
En Jupyter desde Binder: puede experimentar con código de C# en un cuaderno de Jupyter en Binder.
En el equipo local: una vez que haya explorado en línea, puede descargar el SDK de .NET Core y compilar
programas en su equipo.
Todos los tutoriales de introducción posteriores a la lección Hola mundo se encuentran disponibles mediante la
experiencia de explorador en línea o en el entorno de desarrollo local. Al final de cada tutorial, decida si desea
continuar con la siguiente lección en línea o en su propia máquina. Hay vínculos que le ayudarán a configurar el
entorno y continuar con el siguiente tutorial en su máquina.
Hola mundo
En el tutorial Hola mundo, creará el programa de C# más básico. Explorará el tipo string y cómo trabajar con
texto. También puede usar la ruta de acceso en Microsoft Learn o en Jupyter desde Binder.
Números en C#
En el tutorial Números en C#, obtendrá información sobre cómo se almacenan los números en los equipos y
cómo realizar cálculos con distintos tipos numéricos. Conocerá los datos básicos sobre cómo realizar redondeos
y cálculos matemáticos con C#. Este tutorial también está disponible para ejecutarse localmente en su máquina.
En este tutorial se asume que ha completado la lección Hola mundo.
Bifurcaciones y bucles
En el tutorial Ramas y bucles se explican los datos básicos sobre la selección de diferentes rutas de acceso de la
ejecución del código en función de los valores almacenados en variables. Aprenderá los datos básicos del flujo
de control, es decir, cómo los programas toman decisiones y eligen distintas acciones. Este tutorial también está
disponible para ejecutarse localmente en su máquina.
En este tutorial se asume que ha completado las lecciones Hola mundo y Números en C#.
Colección de listas
En la lección Colección de listas se ofrece información general sobre el tipo de colección de listas que almacena
secuencias de datos. Se explica cómo agregar y quitar elementos, buscarlos y ordenar las listas. Explorará los
diferentes tipos de listas. Este tutorial también está disponible para ejecutarse localmente en su máquina.
En este tutorial se asume que ha completado las lecciones que se muestran anteriormente.
El primer paso en la ejecución de un tutorial en el equipo es configurar un entorno de desarrollo. Pruebe una de
las siguientes alternativas:
Para usar la CLI de .NET y su elección de texto o editor de código, consulte el tutorial de .NET Hola mundo en
10 minutos. En este tutorial se incluyen instrucciones para configurar un entorno de desarrollo en Windows,
Linux o macOS.
Para usar la CLI de .NET y Visual Studio Code, instale el SDK de .NET y Visual Studio Code.
Para usar Visual Studio 2019, consulte Tutorial: Cree una aplicación de consola de C# sencilla en
Visual Studio.
Si usa Visual Studio 2019 para estos tutoriales, elegirá una selección de menú de Visual Studio cuando un
tutorial le indique que ejecute uno de estos comandos de la CLI:
Archivo > Nuevo > Proyecto crea una aplicación.
Compilar > Compilar solución crea el arcivo ejecutable.
Depurar > Iniciar sin depurar ejecuta el archivo ejecutable.
Números en C#
En el tutorial Números en C#, obtendrá información sobre cómo se almacenan los números en los equipos y
cómo realizar cálculos con distintos tipos numéricos. Conocerá los datos básicos sobre cómo realizar redondeos
y cálculos matemáticos con C#.
En este tutorial se asume que ha completado la lección Hola mundo.
Bifurcaciones y bucles
En el tutorial Ramas y bucles se explican los datos básicos sobre la selección de diferentes rutas de acceso de la
ejecución del código en función de los valores almacenados en variables. Aprenderá los datos básicos del flujo
de control, es decir, cómo los programas toman decisiones y eligen distintas acciones.
En este tutorial se supone que ha completado las lecciones Hola mundo y Números en C#.
Colección de listas
En la lección Colección de listas se ofrece información general sobre el tipo de colección de listas que almacena
secuencias de datos. Se explica cómo agregar y quitar elementos, buscarlos y ordenar las listas. Explorará los
diferentes tipos de listas.
En este tutorial se presupone que ha completado las lecciones que se muestran anteriormente.
Manipular números enteros y de punto flotante en
C#
16/09/2021 • 10 minutes to read
En este tutorial se explican los tipos numéricos en C# de manera interactiva. Escribirá pequeñas cantidades de
código y luego compilará y ejecutará ese código. El tutorial contiene una serie de lecciones que ofrecen
información detallada sobre los números y las operaciones matemáticas en C#. En ellas se enseñan los aspectos
básicos del lenguaje C#.
Requisitos previos
En el tutorial se espera que tenga una máquina configurada para el desarrollo local. En Windows, Linux o
macOS, puede usar la CLI de .NET para crear, compilar y ejecutar aplicaciones. En Mac o Windows, también
puede usar Visual Studio 2019. Para obtener instrucciones de configuración, consulte cómo configurar el
entorno local.
IMPORTANT
Las plantillas de C# para .NET 6 usan instrucciones de nivel superior. Es posible que la aplicación no coincida con el código
de este artículo si ya ha actualizado a las versiones preliminares de .NET 6. Para obtener más información, consulte el
artículo Las nuevas plantillas de C# generan instrucciones de nivel superior.
El SDK de .NET 6 también agrega un conjunto de directivas de global using implícitas para proyectos que usan los SDK
siguientes:
Microsoft.NET.Sdk
Microsoft.NET.Sdk.Web
Microsoft.NET.Sdk.Worker
Estas directivas de global using implícitas incluyen los espacios de nombres más comunes para el tipo de proyecto.
Abra Program.cs en su editor favorito y reemplace el contenido del archivo por el código siguiente:
using System;
int a = 18;
int b = 6;
int c = a + b;
Console.WriteLine(c);
// subtraction
c = a - b;
Console.WriteLine(c);
// multiplication
c = a * b;
Console.WriteLine(c);
// division
c = a / b;
Console.WriteLine(c);
TIP
Cuando explore C# o cualquier otro lenguaje de programación, cometerá errores al escribir código. El compilador
buscará dichos errores y los notificará. Si la salida contiene mensajes de error, revise detenidamente el código de ejemplo y
el código de la ventana para saber qué debe corregir. Este ejercicio le ayudará a aprender la estructura del código de C#.
Ha terminado el primer paso. Antes comenzar con la siguiente sección, se va a mover el código actual a un
método independiente. Un método es una serie de instrucciones agrupadas a la que se ha puesto un nombre.
Llame a un método escribiendo el nombre del método seguido de () . La organización del código en métodos
facilita empezar a trabajar con un ejemplo nuevo. Cuando termine, el código debe tener un aspecto similar al
siguiente:
using System;
WorkWithIntegers();
void WorkWithIntegers()
{
int a = 18;
int b = 6;
int c = a + b;
Console.WriteLine(c);
// subtraction
c = a - b;
Console.WriteLine(c);
// multiplication
c = a * b;
Console.WriteLine(c);
// division
c = a / b;
Console.WriteLine(c);
}
//WorkWithIntegers();
El // inicia un comentario en C#. Los comentarios son cualquier texto que desea mantener en el código
fuente pero que no se ejecuta como código. El compilador no genera ningún código ejecutable a partir de
comentarios. Dado que WorkWithIntegers() es un método, solo tiene que comentar una línea.
El lenguaje C# define la prioridad de las diferentes operaciones matemáticas con reglas compatibles con las
reglas aprendidas en las operaciones matemáticas. La multiplicación y división tienen prioridad sobre la suma y
resta. Explórelo mediante la adición del código siguiente tras la llamada a dotnet run y la ejecución de
WorkWithIntegers() :
int a = 5;
int b = 4;
int c = 2;
int d = a + b * c;
Console.WriteLine(d);
d = (a + b) * c;
Console.WriteLine(d);
Combine muchas operaciones distintas para indagar más. Agregue algo similar a las líneas siguientes. Pruebe
dotnet run de nuevo.
d = (a + b) - 6 * c + (12 * 4) / 3 + 12;
Console.WriteLine(d);
Puede que haya observado un comportamiento interesante de los enteros. La división de enteros siempre
genera un entero como resultado, incluso cuando se espera que el resultado incluya un decimal o una parte de
una fracción.
Si no ha observado este comportamiento, pruebe este código:
int e = 7;
int f = 4;
int g = 3;
int h = (e + f) / g;
Console.WriteLine(h);
// WorkWithIntegers();
OrderPrecedence();
void WorkWithIntegers()
{
int a = 18;
int b = 6;
int c = a + b;
Console.WriteLine(c);
// subtraction
c = a - b;
Console.WriteLine(c);
// multiplication
c = a * b;
Console.WriteLine(c);
// division
c = a / b;
Console.WriteLine(c);
}
void OrderPrecedence()
{
int a = 5;
int b = 4;
int c = 2;
int d = a + b * c;
Console.WriteLine(d);
d = (a + b) * c;
Console.WriteLine(d);
d = (a + b) - 6 * c + (12 * 4) / 3 + 12;
Console.WriteLine(d);
int e = 7;
int f = 4;
int g = 3;
int h = (e + f) / g;
Console.WriteLine(h);
}
int a = 7;
int b = 4;
int c = 3;
int d = (a + b) / c;
int e = (a + b) % c;
Console.WriteLine($"quotient: {d}");
Console.WriteLine($"remainder: {e}");
El tipo de entero de C# difiere de los enteros matemáticos en un aspecto: el tipo int tiene límites mínimo y
máximo. Agregue este código para ver esos límites:
Si un cálculo genera un valor que supera los límites, se producirá una condición de subdesbordamiento o
desbordamiento . La respuesta parece ajustarse de un límite al otro. Agregue estas dos líneas para ver un
ejemplo:
Tenga en cuenta que la respuesta está muy próxima al entero mínimo (negativo). Es lo mismo que min + 2 . La
operación de suma desbordó los valores permitidos para los enteros. La respuesta es un número negativo muy
grande porque un desbordamiento "se ajusta" desde el valor de entero más alto posible al más bajo.
Hay otros tipos numéricos con distintos límites y precisiones que podría usar si el tipo int no satisface sus
necesidades. Vamos a explorar ahora esos otros tipos. Antes de comenzar la siguiente sección, mueva el código
que escribió en esta sección a un método independiente. Denomínelo TestLimits .
double a = 5;
double b = 4;
double c = 2;
double d = (a + b) / c;
Console.WriteLine(d);
Tenga en cuenta que la respuesta incluye la parte decimal del cociente. Pruebe una expresión algo más
complicada con tipos double:
double e = 19;
double f = 23;
double g = 8;
double h = (e + f) / g;
Console.WriteLine(h);
El intervalo de un valor double es mucho más amplio que en el caso de los valores enteros. Pruebe el código
siguiente debajo del que ha escrito hasta ahora:
Tenga en cuenta que el intervalo es más pequeño que con el tipo double . Puede observar una precisión mayor
con el tipo decimal si prueba el siguiente código:
double a = 1.0;
double b = 3.0;
Console.WriteLine(a / b);
decimal c = 1.0M;
decimal d = 3.0M;
Console.WriteLine(c / d);
El sufijo M en los números es la forma de indicar que una constante debe usar el tipo decimal . De no ser así, el
compilador asume el tipo de double .
NOTE
La letra M se eligió como la letra más distintiva visualmente entre las palabras clave double y decimal .
Observe que la expresión matemática con el tipo decimal tiene más dígitos a la derecha del punto decimal.
Desafío
Ahora que ya conoce los diferentes tipos numéricos, escriba código para calcular el área de un círculo cuyo
radio sea de 2,50 centímetros. Recuerde que el área de un circulo es igual al valor de su radio elevado al
cuadrado multiplicado por Pi. Sugerencia: .NET contiene una constante de Pi, Math.PI, que puede usar para ese
valor. Math.PI, al igual que todas las constantes declaradas en el espacio de nombres System.Math , es un valor
double . Por ese motivo, debe usar double en lugar de valores decimal para este desafío.
Debe obtener una respuesta entre 19 y 20. Puede comprobar la respuesta si consulta el ejemplo de código
terminado en GitHub.
Si lo desea, pruebe con otras fórmulas.
Ha completado el inicio rápido "Números en C#". Puede continuar con la guía de inicio rápido Ramas y bucles
en su propio entorno de desarrollo.
En estos temas encontrará más información sobre los números en C#:
Tipos numéricos integrales
Tipos numéricos de punto flotante
Conversiones numéricas integradas
Obtenga información sobre la lógica condicional
con instrucciones de rama y bucle
16/09/2021 • 10 minutes to read
En este tutorial se enseña a escribir código que analiza variables y cambia la ruta de acceso de ejecución en
función de dichas variables. Escriba código de C# y vea los resultados de la compilación y la ejecución. El tutorial
contiene una serie de lecciones en las que se analizan las construcciones de bifurcaciones y bucles en C#. En
ellas se enseñan los aspectos básicos del lenguaje C#.
Requisitos previos
En el tutorial se espera que tenga una máquina configurada para el desarrollo local. En Windows, Linux o
macOS, puede usar la CLI de .NET para crear, compilar y ejecutar aplicaciones. En Mac y Windows, también
puede usar Visual Studio 2019. Para obtener instrucciones de configuración, consulte cómo configurar el
entorno local.
IMPORTANT
Las plantillas de C# para .NET 6 usan instrucciones de nivel superior. Es posible que la aplicación no coincida con el código
de este artículo si ya ha actualizado a las versiones preliminares de .NET 6. Para obtener más información, consulte el
artículo Las nuevas plantillas de C# generan instrucciones de nivel superior.
El SDK de .NET 6 también agrega un conjunto de directivas de global using implícitas para proyectos que usan los SDK
siguientes:
Microsoft.NET.Sdk
Microsoft.NET.Sdk.Web
Microsoft.NET.Sdk.Worker
Estas directivas de global using implícitas incluyen los espacios de nombres más comunes para el tipo de proyecto.
Este comando crea una nueva aplicación de consola de .NET en el directorio actual. Abra Program.cs en su editor
favorito y reemplace el contenido por el código siguiente:
using System;
int a = 5;
int b = 6;
if (a + b > 10)
Console.WriteLine("The answer is greater than 10.");
Pruebe este código escribiendo dotnet run en la ventana de la consola. Debería ver el mensaje "The answer is
greater than 10" (La respuesta es mayor que 10) impreso en la consola. Modifique la declaración de b para que
el resultado de la suma sea menor que diez:
int b = 3;
Escriba dotnet run de nuevo. Como la respuesta es menor que diez, no se imprime nada. La condición que
está probando es false. No tiene ningún código para ejecutar porque solo ha escrito una de las bifurcaciones
posibles para una instrucción if : la bifurcación true.
TIP
Cuando explore C# o cualquier otro lenguaje de programación, cometerá errores al escribir código. El compilador buscará
dichos errores y los notificará. Fíjese en la salida de error y en el código que generó el error. El error del compilador
normalmente puede ayudarle a encontrar el problema.
En este primer ejemplo se muestran la potencia de if y los tipos booleanos. Un booleano es una variable que
puede tener uno de estos dos valores: true o false . C# define un tipo especial bool para las variables
booleanas. La instrucción if comprueba el valor de bool . Cuando el valor es true , se ejecuta la instrucción
que sigue a if . De lo contrario, se omite. Este proceso de comprobación de condiciones y ejecución de
instrucciones en función de esas condiciones es muy eficaz.
int a = 5;
int b = 3;
if (a + b > 10)
Console.WriteLine("The answer is greater than 10");
else
Console.WriteLine("The answer is not greater than 10");
La instrucción que sigue a la palabra clave else se ejecuta solo si la condición de prueba es false . La
combinación de if y else con condiciones booleanas ofrece toda la eficacia necesaria para administrar una
condición true y false simultáneamente.
IMPORTANT
La sangría debajo de las instrucciones if y else se utiliza para los lectores humanos. El lenguaje C# no considera
significativos los espacios en blanco ni las sangrías. La instrucción que sigue a la palabra clave if o else se ejecutará
en función de la condición. Todos los ejemplos de este tutorial siguen una práctica común para aplicar sangría a las líneas
en función del flujo de control de las instrucciones.
Dado que la sangría no es significativa, debe usar { y } para indicar si desea que más de una instrucción
forme parte del bloque que se ejecuta de forma condicional. Los programadores de C# suelen usar esas llaves
en todas las cláusulas if y else . El siguiente ejemplo es igual que el que acaba de crear. Modifique el código
anterior para que coincida con el código siguiente:
int a = 5;
int b = 3;
if (a + b > 10)
{
Console.WriteLine("The answer is greater than 10");
}
else
{
Console.WriteLine("The answer is not greater than 10");
}
TIP
En el resto de este tutorial, todos los ejemplos de código incluyen las llaves, según las prácticas aceptadas.
Puede probar condiciones más complicadas. Agregue el código siguiente después del que ha escrito hasta
ahora:
int c = 4;
if ((a + b + c > 10) && (a == b))
{
Console.WriteLine("The answer is greater than 10");
Console.WriteLine("And the first number is equal to the second");
}
else
{
Console.WriteLine("The answer is not greater than 10");
Console.WriteLine("Or the first number is not equal to the second");
}
El símbolo == prueba la igualdad. Usar == permite distinguir la prueba de igualdad de la asignación, que verá
en a = 5 .
&& representa "y". Significa que ambas condiciones deben cumplirse para ejecutar la instrucción en la
bifurcación true. En estos ejemplos también se muestra que puede tener varias instrucciones en cada
bifurcación condicional, siempre que las encierre entre { y } . También puede usar || para representar "o".
Agregue el código siguiente antes del que ha escrito hasta ahora:
Modifique los valores de a , b y c y cambie entre && y || para explorar. Obtendrá más conocimientos
sobre el funcionamiento de los operadores && y || .
Ha terminado el primer paso. Antes comenzar con la siguiente sección, se va a mover el código actual a un
método independiente. Con este paso, resulta más fácil empezar con un nuevo ejemplo. Coloque el código
existente en un método denominado ExploreIf() . Llámelo desde la parte superior del programa. Cuando
termine, el código debe tener un aspecto similar al siguiente:
using System;
ExploreIf();
void ExploreIf()
{
int a = 5;
int b = 3;
if (a + b > 10)
{
Console.WriteLine("The answer is greater than 10");
}
else
{
Console.WriteLine("The answer is not greater than 10");
}
int c = 4;
if ((a + b + c > 10) && (a > b))
{
Console.WriteLine("The answer is greater than 10");
Console.WriteLine("And the first number is greater than the second");
}
else
{
Console.WriteLine("The answer is not greater than 10");
Console.WriteLine("Or the first number is not greater than the second");
}
Convierta en comentario la llamada a ExploreIf() . De este modo la salida estará menos saturada a medida que
trabaje en esta sección:
//ExploreIf();
El // inicia un comentario en C#. Los comentarios son cualquier texto que desea mantener en el código
fuente pero que no se ejecuta como código. El compilador no genera ningún código ejecutable a partir de
comentarios.
La instrucción while comprueba una condición y ejecuta la instrucción o el bloque de instrucciones que
aparece después de while . La comprobación de la condición y la ejecución de dichas instrucciones se repetirán
hasta que la condición sea false.
En este ejemplo aparece otro operador nuevo. El código ++ que aparece después de la variable counter es el
operador de incremento . Suma un valor de uno al valor de counter y almacena dicho valor en la variable de
counter .
IMPORTANT
Asegúrese de que la condición del bucle while cambia a false mientras ejecuta el código. En caso contrario, se crea un
bucle infinito donde nunca finaliza el programa. Esto no está demostrado en este ejemplo, ya que tendrá que forzar al
programa a cerrar mediante CTRL-C u otros medios.
El bucle while prueba la condición antes de ejecutar el código que sigue a while . El bucle do ... while
primero ejecuta el código y después comprueba la condición. El bucle do while se muestra en el código
siguiente:
int counter = 0;
do
{
Console.WriteLine($"Hello World! The counter is {counter}");
counter++;
} while (counter < 10);
El código anterior funciona de la misma forma que los bucles while y do que ya ha usado. La instrucción for
consta de tres partes que controlan su funcionamiento.
La primera parte es el inicializador de for : int index = 0; declara que index es la variable de bucle y
establece su valor inicial en 0 .
La parte central es la condición de for : index < 10 declara que este bucle for debe continuar ejecutándose
mientras que el valor del contador sea menor que diez.
La última parte es el iterador de for : index++ especifica cómo modificar la variable de bucle después de
ejecutar el bloque que sigue a la instrucción for . En este caso, especifica que index debe incrementarse en
uno cada vez que el bloque se ejecuta.
Experimente usted mismo. Pruebe cada una de las siguientes variaciones:
Cambie el inicializador para que se inicie en un valor distinto.
Cambie la condición para que se detenga en un valor diferente.
Cuando haya terminado, escriba algo de código para practicar con lo que ha aprendido.
Hay otra instrucción de bucle que no se trata en este tutorial: la instrucción foreach . La instrucción foreach
repite su instrucción con cada elemento de una secuencia de elementos. Se usa más a menudo con colecciones,
por lo que se trata en el siguiente tutorial.
Puede ver que el bucle externo se incrementa una vez con cada ejecución completa del bucle interno. Invierta el
anidamiento de filas y columnas, y vea los cambios por sí mismo. Cuando haya terminado, coloque el código de
esta sección en un método denominado ExploreLoops() .
En este tutorial de presentación se proporciona una introducción al lenguaje C# y se exponen los conceptos
básicos de la clase List<T>.
Requisitos previos
En el tutorial se espera que tenga una máquina configurada para el desarrollo local. En Windows, Linux o
macOS, puede usar la CLI de .NET para crear, compilar y ejecutar aplicaciones. En Mac y Windows, también
puede usar Visual Studio 2019. Para obtener instrucciones de configuración, consulte cómo configurar el
entorno local.
IMPORTANT
Las plantillas de C# para .NET 6 usan instrucciones de nivel superior. Es posible que la aplicación no coincida con el código
de este artículo si ya ha actualizado a las versiones preliminares de .NET 6. Para obtener más información, consulte el
artículo Las nuevas plantillas de C# generan instrucciones de nivel superior.
El SDK de .NET 6 también agrega un conjunto de directivas de global using implícitas para proyectos que usan los SDK
siguientes:
Microsoft.NET.Sdk
Microsoft.NET.Sdk.Web
Microsoft.NET.Sdk.Worker
Estas directivas de global using implícitas incluyen los espacios de nombres más comunes para el tipo de proyecto.
using System;
using System.Collections.Generic;
Reemplace <name> por su propio nombre. Guarde Program.cs. Escriba dotnet run en la ventana de la consola
para probarlo.
Ha creado una lista de cadenas, ha agregado tres nombres a esa lista y ha impreso los nombres en
MAYÚSCULAS. Los conceptos aplicados ya se han aprendido en los tutoriales anteriores para recorrer en bucle
la lista.
El código para mostrar los nombres usa la característica interpolación de cadenas. Si un valor de string va
precedido del carácter $ , significa que puede insertar código de C# en la declaración de cadena. La cadena real
reemplaza a ese código de C# con el valor que genera. En este ejemplo, reemplaza {name.ToUpper()} con cada
nombre, convertido a mayúsculas, porque se llama al método ToUpper.
Vamos a continuar indagando.
Console.WriteLine();
names.Add("Maria");
names.Add("Bill");
names.Remove("Ana");
foreach (var name in names)
{
Console.WriteLine($"Hello {name.ToUpper()}!");
}
Se han agregado dos nombres más al final de la lista. También se ha quitado uno. Guarde el archivo y escriba
dotnet run para probarlo.
List<T> también permite hacer referencia a elementos individuales a través del índice . Coloque el índice entre
los tokens [ y ] después del nombre de la lista. C# utiliza 0 para el primer índice. Agregue este código
directamente después del código que acaba de agregar y pruébelo:
No se puede acceder a un índice si se coloca después del final de la lista. Recuerde que los índices empiezan en
0, por lo que el índice más grande válido es uno menos que el número de elementos de la lista. Puede
comprobar durante cuánto tiempo la lista usa la propiedad Count. Agregue el código siguiente al final del
programa:
Guarde el archivo y vuelva a escribir dotnet run para ver los resultados.
Los elementos de la lista también se pueden ordenar. El método Sort clasifica todos los elementos de la lista en
su orden normal (por orden alfabético si se trata de cadenas). Agregue este código a la parte inferior del
programa:
names.Sort();
foreach (var name in names)
{
Console.WriteLine($"Hello {name.ToUpper()}!");
}
Guarde el archivo y escriba dotnet run para probar esta última versión.
Antes comenzar con la siguiente sección, se va a mover el código actual a un método independiente. Con este
paso, resulta más fácil empezar con un nuevo ejemplo. Coloque todo el código que ha escrito en un nuevo
método denominado WorkWithStrings() . Llame a ese método en la parte superior del programa. Cuando
termine, el código debe tener un aspecto similar al siguiente:
using System;
using System.Collections.Generic;
WorkWithString();
void WorkWithString()
{
var names = new List<string> { "<name>", "Ana", "Felipe" };
foreach (var name in names)
{
Console.WriteLine($"Hello {name.ToUpper()}!");
}
Console.WriteLine();
names.Add("Maria");
names.Add("Bill");
names.Remove("Ana");
foreach (var name in names)
{
Console.WriteLine($"Hello {name.ToUpper()}!");
}
names.Sort();
foreach (var name in names)
{
Console.WriteLine($"Hello {name.ToUpper()}!");
}
}
fibonacciNumbers.Add(previous + previous2);
TIP
Para centrarse solo en esta sección, puede comentar el código que llama a WorkingWithStrings(); . Solo debe colocar
dos caracteres / delante de la llamada, como en: // WorkingWithStrings(); .
Desafío
Trate de recopilar los conceptos que ha aprendido en esta lección y en las anteriores. Amplíe lo que ha creado
hasta el momento con los números de Fibonacci. Pruebe a escribir el código para generar los veinte primeros
números de la secuencia. (Como sugerencia, el 20º número de la serie de Fibonacci es 6765).
Desafío completo
Puede ver un ejemplo de solución en el ejemplo de código terminado en GitHub.
Con cada iteración del bucle, se obtienen los dos últimos enteros de la lista, se suman y se agrega el valor
resultante a la lista. El bucle se repite hasta que se hayan agregado veinte elementos a la lista.
Enhorabuena, ha completado el tutorial sobre las listas. Puede seguir estos tutoriales adicionales en su propio
entorno de desarrollo.
Puede obtener más información sobre cómo trabajar con el tipo List en el artículo de los aspectos básicos de
.NET que trata sobre las colecciones. Ahí también podrá conocer muchos otros tipos de colecciones.
Estructura general de un programa de C#
16/09/2021 • 2 minutes to read
Los programas de C# constan de uno o más archivos. Cada archivo contiene cero o más espacios de nombres.
Un espacio de nombres contiene tipos como clases, estructuras, interfaces, enumeraciones y delegados, u otros
espacios de nombres. El siguiente ejemplo es el esqueleto de un programa de C# que contiene todos estos
elementos.
// A skeleton of a C# program
using System;
namespace YourNamespace
{
class YourClass
{
}
struct YourStruct
{
}
interface IYourInterface
{
}
enum YourEnum
{
}
namespace YourNestedNamespace
{
struct YourStruct
{
}
}
}
En el ejemplo anterior se usan instrucciones de nivel superior para el punto de entrada del programa. Esta
característica se agregó en C# 9. Antes de C# 9, el punto de entrada era un método estático denominado Main ,
como se muestra en el ejemplo siguiente:
// A skeleton of a C# program
using System;
namespace YourNamespace
{
class YourClass
{
}
struct YourStruct
{
}
interface IYourInterface
{
}
enum YourEnum
{
}
namespace YourNestedNamespace
{
struct YourStruct
{
}
}
class Program
{
static void Main(string[] args)
{
//Your program starts here...
Console.WriteLine("Hello world!");
}
}
}
Secciones relacionadas
Obtenga información sobre estos elementos del programa en la sección de tipos de la guía de aspectos básicos:
Clases
Structs
Espacios de nombres
Interfaces
Enumeraciones
Delegados
C# es un lenguaje fuertemente tipado. Todas las variables y constantes tienen un tipo, al igual que todas las
expresiones que se evalúan como un valor. Cada declaración del método especifica un nombre, un número de
parámetros, un tipo y una naturaleza (valor, referencia o salida) para cada parámetro de entrada y para el valor
devuelto. La biblioteca de clases .NET define un conjunto de tipos numéricos integrados, así como tipos más
complejos que representan una amplia variedad de construcciones lógicas, como el sistema de archivos,
conexiones de red, colecciones y matrices de objetos, y fechas. Los programas de C# típicos usan tipos de la
biblioteca de clases, así como tipos definidos por el usuario que modelan los conceptos que son específicos del
dominio del problema del programa.
Entre la información almacenada en un tipo se pueden incluir los siguientes elementos:
El espacio de almacenamiento que requiere una variable del tipo.
Los valores máximo y mínimo que puede representar.
Los miembros (métodos, campos, eventos, etc.) que contiene.
El tipo base del que hereda.
Interfaces que implementa.
Los tipos de operaciones permitidas.
El compilador usa información de tipo para garantizar que todas las operaciones que se realizan en el código
cuentan con seguridad de tipos. Por ejemplo, si declara una variable de tipo int , el compilador le permite usar
la variable en operaciones de suma y resta. Si intenta realizar esas mismas operaciones en una variable de tipo
bool , el compilador genera un error, como se muestra en el siguiente ejemplo:
int a = 5;
int b = a + 2; //OK
// Error. Operator '+' cannot be applied to operands of type 'int' and 'bool'.
int c = a + test;
NOTE
Los desarrolladores de C y C++ deben tener en cuenta que, en C#, bool no se puede convertir en int .
El compilador inserta la información de tipo en el archivo ejecutable como metadatos. Common Language
Runtime (CLR) usa esos metadatos en tiempo de ejecución para garantizar aún más la seguridad de tipos
cuando asigna y reclama memoria.
Los tipos de parámetros del método y los valores devueltos se especifican en la declaración del método. En la
siguiente firma se muestra un método que requiere una variable int como argumento de entrada y devuelve
una cadena:
Después de declarar una variable, no se puede volver a declarar con un nuevo tipo y no se puede asignar un
valor que no sea compatible con su tipo declarado. Por ejemplo, no puede declarar un valor int y, luego,
asignarle un valor booleano de true . En cambio, los valores se pueden convertir en otros tipos, por ejemplo,
cuando se asignan a variables nuevas o se pasan como argumentos de método. El compilador realiza
automáticamente una conversión de tipo que no da lugar a una pérdida de datos. Una conversión que pueda
dar lugar a la pérdida de datos requiere un valor cast en el código fuente.
Para obtener más información, vea Conversiones de tipos.
Tipos integrados
C# proporciona un conjunto estándar de tipos integrados para representar números enteros, valores de punto
flotante, expresiones booleanas, caracteres de texto, valores decimales y otros tipos de datos. También hay tipos
string y object integrados. Estos tipos están disponibles para su uso en cualquier programa de C#. Para
obtener una lista completa de los tipos integrados, vea Tipos integrados.
Tipos personalizados
Puede usar las construcciones struct , class , interface , enum y record para crear sus propios tipos
personalizados. La biblioteca de clases .NET es en sí misma una colección de tipos personalizados que puede
usar en sus propias aplicaciones. De forma predeterminada, los tipos usados con más frecuencia en la biblioteca
de clases están disponibles en cualquier programa de C#. Otros están disponibles solo cuando agrega
explícitamente una referencia de proyecto al ensamblado en el que se definen. Una vez que el compilador tenga
una referencia al ensamblado, puede declarar variables (y constantes) de los tipos declarados en dicho
ensamblado en el código fuente. Para más información, vea Biblioteca de clases .NET.
NOTE
Puede ver que los tipos utilizados con mayor frecuencia están organizados en el espacio de nombres System. Sin
embargo, el espacio de nombres que contiene un tipo no tiene ninguna relación con un tipo de valor o un tipo de
referencia.
Las clases (class) y estructuras (struct) son dos de las construcciones básicas de Common Type System en .NET.
C# 9 agrega registros, que son un tipo de clase. Cada una de ellas es básicamente una estructura de datos que
encapsula un conjunto de datos y comportamientos que forman un conjunto como una unidad lógica. Los datos
y comportamientos son los miembros de la clase, estructura o registro, e incluyen sus métodos, propiedades y
eventos, entre otros elementos, como se muestra más adelante en este artículo.
Una declaración de clase, estructura o registro es como un plano que se utiliza para crear instancias u objetos en
tiempo de ejecución. Si define una clase, una estructura o un registro denominado Person , Person es el
nombre del tipo. Si declara e inicializa una variable p de tipo Person , se dice que p es un objeto o instancia
de Person . Se pueden crear varias instancias del mismo tipo Person , y cada instancia tiene diferentes valores
en sus propiedades y campos.
Una clase o un registro es un tipo de referencia. Cuando se crea un objeto del tipo, la variable a la que se asigna
el objeto contiene solo una referencia a esa memoria. Cuando la referencia de objeto se asigna a una nueva
variable, la nueva variable hace referencia al objeto original. Los cambios realizados en una variable se reflejan
en la otra variable porque ambas hacen referencia a los mismos datos.
Una estructura es un tipo de valor. Cuando se crea una estructura, la variable a la que se asigna la estructura
contiene los datos reales de ella. Cuando la estructura se asigna a una nueva variable, se copia. Por lo tanto, la
nueva variable y la variable original contienen dos copias independientes de los mismos datos. Los cambios
realizados en una copia no afectan a la otra copia.
En general, las clases se utilizan para modelar comportamientos más complejos, o datos que se prevén
modificar después de haber creado un objeto de clase. Las estructuras son más adecuadas para las estructuras
de datos pequeñas que contienen principalmente datos que no se prevén modificar después de haber creado la
estructura. Los tipos de registro son para estructuras de datos más grandes que contienen principalmente datos
que no se han diseñado para modificarse después de crear el objeto.
Tipos de valor
Los tipos de valor derivan de System.ValueType, el cual deriva de System.Object. Los tipos que derivan de
System.ValueType tienen un comportamiento especial en CLR. Las variables de tipo de valor contienen
directamente sus valores, lo que significa que la memoria se asigna insertada en cualquier contexto en el que se
declare la variable. No se produce ninguna asignación del montón independiente ni sobrecarga de la
recolección de elementos no utilizados para las variables de tipo de valor.
Existen dos categorías de tipos de valor: struct y enum .
Los tipos numéricos integrados son structs y tienen campos y métodos a los que se puede acceder:
Pero se declaran y se les asignan valores como si fueran tipos simples no agregados:
Los tipos de valor están sellados, lo que significa que no se puede derivar un tipo de cualquier tipo de valor, por
ejemplo System.Int32. No se puede definir un struct para que herede de cualquier clase o struct definido por el
usuario porque un struct solo puede heredar de System.ValueType. A pesar de ello, un struct puede implementar
una o más interfaces. Puede convertir un tipo struct en cualquier tipo de interfaz que implemente; esto hace que
una operación de conversión boxing encapsule el struct dentro de un objeto de tipo de referencia en el montón
administrado. Las operaciones de conversión boxing se producen cuando se pasa un tipo de valor a un método
que toma System.Object o cualquier tipo de interfaz como parámetro de entrada. Para obtener más información,
vea Conversión boxing y unboxing.
Puede usar la palabra clave struct para crear sus propios tipos de valor personalizados. Normalmente, un struct
se usa como un contenedor para un pequeño conjunto de variables relacionadas, como se muestra en el
ejemplo siguiente:
public struct Coords
{
public int x, y;
Para más información sobre estructuras, vea Tipos de estructura. Para más información sobre los tipos de valor,
vea Tipos de valor.
La otra categoría de tipos de valor es enum . Una enumeración define un conjunto de constantes integrales con
nombre. Por ejemplo, la enumeración System.IO.FileMode de la biblioteca de clases .NET contiene un conjunto
de enteros constantes con nombre que especifican cómo se debe abrir un archivo. Se define como se muestra
en el ejemplo siguiente:
La constante System.IO.FileMode.Create tiene un valor de 2. Sin embargo, el nombre es mucho más significativo
para los humanos que leen el código fuente y, por esa razón, es mejor utilizar enumeraciones en lugar de
números literales constantes. Para obtener más información, vea System.IO.FileMode.
Todas las enumeraciones se heredan de System.Enum, el cual se hereda de System.ValueType. Todas las reglas
que se aplican a las estructuras también se aplican a las enumeraciones. Para más información sobre las
enumeraciones, vea Tipos de enumeración.
Tipos de referencia
Un tipo que se define como class , record , delegate , matriz o interface es un tipo de referencia. Al declarar
una variable de un tipo de referencia en tiempo de ejecución, esta contendrá el valor null hasta que se cree
explícitamente un objeto mediante el operador new , o bien que se le asigne un objeto creado en otro lugar
mediante new , tal y como se muestra en el ejemplo siguiente:
Una interfaz debe inicializarse junto con un objeto de clase que lo implementa. Si MyClass implementa
IMyInterface , cree una instancia de IMyInterface , tal como se muestra en el ejemplo siguiente:
Cuando se crea el objeto, se asigna la memoria en el montón administrado y la variable solo contiene una
referencia a la ubicación del objeto. Los tipos del montón administrado producen sobrecarga cuando se asignan
y cuando los reclama la función de administración de memoria automática de CLR, conocida como recolección
de elementos no utilizados. En cambio, la recolección de elementos no utilizados también está muy optimizada y
no crea problemas de rendimiento en la mayoría de los escenarios. Para obtener más información sobre la
recolección de elementos no utilizados, vea Administración de memoria automática.
Todas las matrices son tipos de referencia, incluso si sus elementos son tipos de valor. Las matrices derivan de
manera implícita de la clase System.Array, pero el usuario las declara y las usa con la sintaxis simplificada que
proporciona C#, como se muestra en el ejemplo siguiente:
Los tipos de referencia admiten la herencia completamente. Al crear una clase, puede heredar de cualquier otra
interfaz o clase que no esté definida como sellado; y otras clases pueden heredar de la clase e invalidar sus
métodos virtuales. Para obtener más información sobre cómo crear sus clases, vea Clases, estructuras y
registros. Para más información sobre la herencia y los métodos virtuales, vea Herencia.
Tipos genéricos
Los tipos se pueden declarar con uno o varios parámetros de tipo que actúan como un marcador de posición
para el tipo real (el tipo concreto) que proporcionará el código de cliente cuando cree una instancia del tipo.
Estos tipos se denominan tipos genéricos. Por ejemplo, el tipo de .NET System.Collections.Generic.List<T> tiene
un parámetro de tipo al que, por convención, se le denomina T . Cuando crea una instancia del tipo, especifica
el tipo de los objetos que contendrá la lista, por ejemplo, string :
El uso del parámetro de tipo permite reutilizar la misma clase para incluir cualquier tipo de elemento, sin
necesidad de convertir cada elemento en object. Las clases de colección genéricas se denominan colecciones
con establecimiento inflexible de tipos porque el compilador conoce el tipo específico de los elementos de la
colección y puede generar un error en tiempo de compilación si, por ejemplo, intenta agregar un valor entero al
objeto stringList del ejemplo anterior. Para más información, vea Genéricos.
En otros casos, el tipo en tiempo de compilación es diferente, tal y como se muestra en los dos ejemplos
siguientes:
En los dos ejemplos anteriores, el tipo en tiempo de ejecución es string . El tipo en tiempo de compilación es
object en la primera línea y IEnumerable<char> en la segunda.
Si los dos tipos son diferentes para una variable, es importante comprender cuándo se aplican el tipo en tiempo
de compilación y el tipo en tiempo de ejecución. El tipo en tiempo de compilación determina todas las acciones
realizadas por el compilador. Estas acciones del compilador incluyen la resolución de llamadas a métodos, la
resolución de sobrecarga y las conversiones implícitas y explícitas disponibles. El tipo en tiempo de ejecución
determina todas las acciones que se resuelven en tiempo de ejecución. Estas acciones de tiempo de ejecución
incluyen el envío de llamadas a métodos virtuales, la evaluación de expresiones is y switch y otras API de
prueba de tipos. Para comprender mejor cómo interactúa el código con los tipos, debe reconocer qué acción se
aplica a cada tipo.
Secciones relacionadas
Para más información, consulte los siguientes artículos.
Tipos integrados
Tipos de valor
Tipos de referencia
Encapsulación
A veces se hace referencia a la encapsulación como el primer pilar o principio de la programación orientada a
objetos. Según el principio de encapsulación, una clase o una estructura pueden especificar hasta qué punto se
puede acceder a sus miembros para codificar fuera de la clase o la estructura. No se prevé el uso de los métodos
y las variables fuera de la clase, o el ensamblado puede ocultarse para limitar el potencial de los errores de
codificación o de los ataques malintencionados. Para obtener más información, consulte Programación
orientada a objetos (C#).
Miembros
Todos los métodos, campos, constantes, propiedades y eventos deben declararse dentro de un tipo; se les
denomina miembros del tipo. En C#, no hay métodos ni variables globales como en otros lenguajes. Incluso se
debe declarar el punto de entrada de un programa, el método Main , dentro de una clase o estructura (de forma
implícita en el caso de instrucciones de nivel superior).
La lista siguiente incluye los diversos tipos de miembros que se pueden declarar en una clase, estructura o
registro.
Campos
Constantes
Propiedades
Métodos
Constructores
Events
Finalizadores
Indexadores
Operadores
Tipos anidados
Accesibilidad
Algunos métodos y propiedades están diseñados para ser invocables y accesibles desde el código fuera de una
clase o estructura, lo que se conoce como código de cliente. Otros métodos y propiedades pueden estar
indicados exclusivamente para utilizarse en la propia clase o estructura. Es importante limitar la accesibilidad del
código, a fin de que solo el código de cliente previsto pueda acceder a él. Puede usar los siguientes
modificadores de acceso para especificar hasta qué punto los tipos y sus miembros son accesibles para el
código de cliente:
public
protected
internal
protected internal
private
private protected
La accesibilidad predeterminada es private .
Herencia
Las clases (pero no las estructuras) admiten el concepto de herencia. Una clase que deriva de otra clase (la clase
base) contiene automáticamente todos los miembros públicos, protegidos e internos de la clase base, salvo sus
constructores y finalizadores. Para más información, vea Herencia y Polimorfismo.
Las clases pueden declararse como abstract, lo que significa que uno o varios de sus métodos no tienen ninguna
implementación. Aunque no se pueden crear instancias de clases abstractas directamente, pueden servir como
clases base para otras clases que proporcionan la implementación que falta. Las clases también pueden
declararse como sealed para evitar que otras clases hereden de ellas.
Interfaces
Las clases, las estructuras y los registros pueden heredar varias interfaces. Heredar de una interfaz significa que
el tipo implementa todos los métodos definidos en la interfaz. Para más información, vea Interfaces.
Tipos genéricos
Las clases, las estructuras y los registros pueden definirse con uno o varios parámetros de tipo. El código de
cliente proporciona el tipo cuando crea una instancia del tipo. Por ejemplo, la clase List<T> del espacio de
nombres System.Collections.Generic se define con un parámetro de tipo. El código de cliente crea una instancia
de List<string> o List<int> para especificar el tipo que contendrá la lista. Para más información, vea
Genéricos.
Tipos estáticos
Las clases (pero no las estructuras ni los registros) pueden declararse como static . Una clase estática puede
contener solo miembros estáticos y no se puede crear una instancia de ellos con la palabra clave new . Una
copia de la clase se carga en memoria cuando se carga el programa, y sus miembros son accesibles a través del
nombre de clase. Las clases, las estructuras y los registros pueden contener miembros estáticos.
Tipos anidados
Una clase, estructura o registro se puede anidar dentro de otra clase, estructura o registro.
Tipos parciales
Puede definir parte de una clase, estructura o método en un archivo de código y otra parte en un archivo de
código independiente.
Inicializadores de objeto
Puede crear instancias e inicializar objetos de clase o estructura, así como colecciones de objetos, asignando
valores a sus propiedades.
Tipos anónimos
En situaciones donde no es conveniente o necesario crear una clase con nombre, por ejemplo al rellenar una
lista con estructuras de datos que no tiene que conservar o pasar a otro método, utilice los tipos anónimos.
Métodos de extensión.
Puede "extender" una clase sin crear una clase derivada mediante la creación de un tipo independiente cuyos
métodos pueden llamarse como si pertenecieran al tipo original.
Registros
C# 9 presenta el tipo record , un tipo de referencia que se puede crear en lugar de una clase o una estructura.
Los registros son clases con un comportamiento integrado para encapsular datos en tipos inmutables. Un
registro proporciona las siguientes características:
Sintaxis concisa para crear un tipo de referencia con propiedades inmutables.
Igualdad de valores.
Dos variables de un tipo de registro son iguales si las definiciones del tipo de registro son idénticas y si,
en cada campo, los valores de ambos registros son iguales. Esto difiere de las clases, que usan la igualdad
de referencia: dos variables de un tipo de clase son iguales si hacen referencia al mismo objeto.
Sintaxis concisa para la mutación no destructiva.
Una expresión with permite crear una copia de una instancia de registro existente, pero con los valores
de propiedad especificados modificados.
Formato integrado para la presentación.
El método ToString imprime el nombre del tipo de registro y los nombres y valores de las propiedades
públicas.
Compatibilidad con las jerarquías de herencia.
La herencia se admite porque un registro es una clase encubierta, no una estructura.
Para obtener más información, consulte Registros.
Las características de control de excepciones del lenguaje C# le ayudan a afrontar cualquier situación inesperada
o excepcional que se produce cuando se ejecuta un programa. El control de excepciones usa las palabras clave
try , catch y finally para intentar realizar acciones que pueden no completarse correctamente, para
controlar errores cuando decide que es razonable hacerlo y para limpiar recursos más adelante. Las excepciones
las puede generar Common Language Runtime (CLR), .NET, bibliotecas de terceros o el código de aplicación. Las
excepciones se crean mediante el uso de la palabra clave throw .
En muchos casos, una excepción la puede no producir un método al que el código ha llamado directamente,
sino otro método más bajo en la pila de llamadas. Cuando se genera una excepción, CLR desenreda la pila,
busca un método con un bloque catch para el tipo de excepción específico y ejecuta el primer bloque catch
que encuentra. Si no encuentra ningún bloque catch adecuado en cualquier parte de la pila de llamadas,
finalizará el proceso y mostrará un mensaje al usuario.
En este ejemplo, un método prueba a hacer la división entre cero y detecta el error. Sin el control de excepciones,
este programa finalizaría con un error DivideByZeroException no controlada .
try
{
result = SafeDivision(a, b);
Console.WriteLine("{0} divided by {1} = {2}", a, b, result);
}
catch (DivideByZeroException)
{
Console.WriteLine("Attempted divide by zero.");
}
}
}
Vea también
SystemException
Palabras clave de C#
throw
try-catch
try-finally
try-catch-finally
Excepciones
Novedades de C# 10.0
16/09/2021 • 3 minutes to read
IMPORTANT
En este artículo se describen las características disponibles en C# 10.0 a partir de .NET 6 Preview 7. La documentación de
las mejoras de C# 10.0 está en curso. Puede comprobar el progreso de la documentación en este proyecto.
En C# 10.0 se agregan las siguientes características y mejoras al lenguaje C# (a partir de .NET 6 Preview 7):
Directivas global using
Declaración de espacios de nombres con ámbito de archivo
Patrones de propiedades extendidos
Se permiten cadenas interpoladas const
Los tipos de registro pueden sellar ToString()
Se permite la asignación y la declaración en la misma desconstrucción
Se permite el atributo AsyncMethodBuilder en los métodos
Algunas de las características que puede probar solo están disponibles cuando establece la versión del lenguaje
en "versión preliminar". Es posible que estas características tengan más mejoras en futuras versiones
preliminares antes de la publicación de .NET 6.0.
C# 10.0 es compatible con .NET 6 . Para obtener más información, vea Control de versiones del lenguaje C#.
Puede descargar el SDK de .NET 6.0 más reciente de la página de descargas de .NET. También puede descargar
Visual Studio 2022 Preview, que incluye el SDK de la versión preliminar de .NET 6.0.
namespace MyNamespace;
Esta nueva sintaxis ahorra espacio horizontal y vertical para las declaraciones namespace más comunes.
{ Prop1.Prop2: pattern }
es válido en C# 10.0 y versiones posteriores, y equivalente a
NOTE
Cuando se usa NET 6.0 Preview 5, para esta característica es necesario establecer el elemento <LangVersion> del archivo
csproj en preview .
NOTE
Cuando se usa NET 6.0 Preview 5, para esta característica es necesario establecer el elemento <LangVersion> del archivo
csproj en preview .
// Initialization:
(int x, int y) = point;
// assignment:
int x1 = 0;
int y1 = 0;
(x1, y1) = point;
NOTE
Cuando se usa NET 6.0 Preview 5, para esta característica es necesario establecer el elemento <LangVersion> del archivo
csproj en preview .
Tipos de registro
C# 9.0 presenta los tipos de registro . Se usa la palabra clave record para definir un tipo de referencia que
proporciona funcionalidad integrada para encapsular los datos. Puede crear tipos de registros con propiedades
inmutables mediante parámetros posicionales o sintaxis de propiedades estándar:
Aunque los registros pueden ser mutables, están destinados principalmente a admitir modelos de datos
inmutables. El tipo de registro ofrece las siguientes características:
Sintaxis concisa para crear un tipo de referencia con propiedades inmutables
Comportamiento útil para un tipo de referencia centrado en datos:
Igualdad de valores
Sintaxis concisa para la mutación no destructiva
Formato integrado para la presentación
Compatibilidad con las jerarquías de herencia
Puede utilizar tipos de estructura para diseñar tipos centrados en datos que proporcionen igualdad de valores y
un comportamiento escaso o inexistente. Pero, en el caso de los modelos de datos relativamente grandes, los
tipos de estructura tienen algunas desventajas:
No admiten la herencia.
Son menos eficaces a la hora de determinar la igualdad de valores. En el caso de los tipos de valor, el método
ValueType.Equals usa la reflexión para buscar todos los campos. En el caso de los registros, el compilador
genera el método Equals . En la práctica, la implementación de la igualdad de valores en los registros es
bastante más rápida.
Usan más memoria en algunos escenarios, ya que cada instancia tiene una copia completa de todos los
datos. Los tipos de registro son tipos de referencia, por lo que una instancia de registro solo contiene una
referencia a los datos.
Sintaxis posicional para la definición de propiedad
Puede usar parámetros posicionales para declarar propiedades de un registro e inicializar los valores de
propiedad al crear una instancia:
Cuando se usa la sintaxis posicional para la definición de propiedad, el compilador crea lo siguiente:
Una propiedad pública implementada automáticamente de solo inicialización para cada parámetro
posicional proporcionado en la declaración de registro. Una propiedad de solo inicialización solo se puede
establecer en el constructor o mediante un inicializador de propiedad.
Un constructor primario cuyos parámetros coinciden con los parámetros posicionales en la declaración del
registro.
Un método Deconstruct con un parámetro out para cada parámetro posicional proporcionado en la
declaración de registro.
Para obtener más información, vea Sintaxis posicional en el artículo de referencia del lenguaje C# acerca de los
registros.
Inmutabilidad
Un tipo de registro no es necesariamente inmutable. Puede declarar propiedades con descriptores de acceso
set y campos que no sean readonly . Sin embargo, aunque los registros pueden ser mutables, facilitan la
creación de modelos de datos inmutables. Las propiedades que se crean mediante la sintaxis posicional son
inmutables.
La inmutabilidad puede resultar útil si quiere que un tipo centrado en datos sea seguro para subprocesos o un
código hash quede igual en una tabla hash. Puede impedir que se produzcan errores cuando se pasa un
argumento por referencia a un método y el método cambia inesperadamente el valor del argumento.
Las características exclusivas de los tipos de registro se implementan mediante métodos sintetizados por el
compilador, y ninguno de estos métodos pone en peligro la inmutabilidad mediante la modificación del estado
del objeto.
Igualdad de valores
La igualdad de valores significa que dos variables de un tipo de registro son iguales si los tipos coinciden y
todos los valores de propiedad y campo coinciden. Para otros tipos de referencia, la igualdad significa identidad.
Es decir, dos variables de un tipo de referencia son iguales si hacen referencia al mismo objeto.
En el ejemplo siguiente se muestra la igualdad de valores de tipos de registro:
person1.PhoneNumbers[0] = "555-1234";
Console.WriteLine(person1 == person2); // output: True
En los tipos class , podría invalidar manualmente los métodos y los operadores de igualdad para lograr la
igualdad de valores, pero el desarrollo y las pruebas de ese código serían lentos y propensos a errores. Al tener
esta funcionalidad integrada, se evitan los errores que resultarían de olvidarse de actualizar el código de
invalidación personalizado cuando se agreguen o cambien propiedades o campos.
Para obtener más información, vea Igualdad de valores en el artículo de referencia del lenguaje C# acerca de los
registros.
Mutación no destructiva
Si necesita mutar propiedades inmutables de una instancia de registro, puede usar una expresión with para
lograr una mutación no destructiva. Una expresión with crea una instancia de registro que es una copia de una
instancia de registro existente, con las propiedades y los campos especificados modificados. Use la sintaxis del
inicializador de objeto para especificar los valores que se van a cambiar, como se muestra en el ejemplo
siguiente:
public record Person(string FirstName, string LastName)
{
public string[] PhoneNumbers { get; init; }
}
Para obtener más información, vea Mutación no destructiva en el artículo de referencia del lenguaje C# acerca
de los registros.
Formato integrado para la presentación
Los tipos de registros tienen un método ToString generado por el compilador que muestra los nombres y los
valores de las propiedades y los campos públicos. El método ToString devuelve una cadena con el formato
siguiente:
<record type name> { <property name> = <value>, <property name> = <value>, ...}
En el caso de los tipos de referencia, se muestra el nombre del tipo del objeto al que hace referencia la
propiedad en lugar del valor de propiedad. En el ejemplo siguiente, la matriz es un tipo de referencia, por lo que
se muestra System.String[] en lugar de los valores de los elementos de matriz reales:
Para obtener más información, vea Formato integrado en el artículo de referencia del lenguaje C# acerca de los
registros.
Herencia
Un registro puede heredar de otro registro. Sin embargo, un registro no puede heredar de una clase, y una clase
no puede heredar de un registro.
En el ejemplo siguiente se muestra la herencia con la sintaxis de la propiedad posicional:
public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public static void Main()
{
Person teacher = new Teacher("Nancy", "Davolio", 3);
Console.WriteLine(teacher);
// output: Teacher { FirstName = Nancy, LastName = Davolio, Grade = 3 }
}
Para que dos variables de registro sean iguales, el tipo en tiempo de ejecución debe ser el mismo. Los tipos de
las variables contenedoras podrían ser diferentes. Esto se muestra en el siguiente código de ejemplo:
En el ejemplo, todas las instancias tienen las mismas propiedades y los mismos valores de propiedad. Pero
student == teacher devuelve False aunque ambas sean variables de tipo Person . Y student == student2
devuelve True aunque una sea una variable Person y otra sea una variable Student .
Todas las propiedades y los campos públicos de los tipos derivados y base se incluyen en la salida ToString ,
como se muestra en el ejemplo siguiente:
Para obtener más información, vea Herencia en el artículo de referencia del lenguaje C# acerca de los registros.
Los autores de la llamada pueden usar la sintaxis de inicializador de propiedades para establecer los valores, a la
vez que conservan la inmutabilidad:
Un intento de cambiar una observación después de la inicialización genera un error del compilador:
// Error! CS8852.
now.TemperatureInCelsius = 18;
Los establecedores de solo inicialización pueden ser útiles para establecer las propiedades de clase base de las
clases derivadas. También pueden establecer propiedades derivadas mediante asistentes en una clase base. Los
registros posicionales declaran propiedades mediante establecedores de solo inicialización. Esos establecedores
se usan en expresiones with. Puede declarar establecedores de solo inicialización para cualquier objeto class ,
struct o record que defina.
namespace HelloWorld
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}
Solo hay una línea de código que haga algo. Con las instrucciones de nivel superior, puede reemplazar todo lo
que sea reutilizable por la directiva using y la línea única que realiza el trabajo:
using System;
Console.WriteLine("Hello World!");
Si quisiera un programa de una línea, podría quitar la directiva using y usar el nombre de tipo completo:
System.Console.WriteLine("Hello World!");
Solo un archivo de la aplicación puede usar instrucciones de nivel superior. Si el compilador encuentra
instrucciones de nivel superior en varios archivos de código fuente, se trata de un error. También es un error si
combina instrucciones de nivel superior con un método de punto de entrada de programa declarado,
normalmente Main . En cierto sentido, puede pensar que un archivo contiene las instrucciones que
normalmente se encontrarían en el método Main de una clase Program .
Uno de los usos más comunes de esta característica es la creación de materiales educativos. Los desarrolladores
principiantes de C# pueden escribir el programa "Hola mundo" en una o dos líneas de código. No se necesitan
pasos adicionales. Pero los desarrolladores veteranos también encontrarán muchas aplicaciones a esta
característica. Las instrucciones de nivel superior permiten una experiencia de experimentación de tipo script
similar a la que proporcionan los cuadernos de Jupyter Notebook. Las instrucciones de nivel superior son
excelentes para programas y utilidades de consola pequeños. Azure Functions es un caso de uso ideal para las
instrucciones de nivel superior.
Y sobre todo, las instrucciones de nivel superior no limitan el ámbito ni la complejidad de la aplicación. Estas
instrucciones pueden acceder a cualquier clase de .NET o usarla. Tampoco limitan el uso de argumentos de línea
de comandos ni de valores devueltos. Las instrucciones de nivel superior pueden acceder a una matriz de
cadenas denominada args . Si las instrucciones de nivel superior devuelven un valor entero, ese valor se
convierte en el código devuelto entero de un método Main sintetizado. Las instrucciones de nivel superior
pueden contener expresiones asincrónicas. En ese caso, el punto de entrada sintetizado devuelve un objeto
Task , o Task<int> .
Para obtener más información, vea Instrucciones de nivel superior en la Guía de programación de C#.
Con paréntesis opcionales para que quede claro que and tiene mayor prioridad que or :
Uno de los usos más comunes es una nueva sintaxis para una comprobación NULL:
if (e is not null)
{
// ...
}
Cualquiera de estos patrones se puede usar en cualquier contexto en el que se permitan patrones: expresiones
de patrón is , expresiones switch , patrones anidados y el patrón de la etiqueta case de una instrucción
switch .
Rendimiento e interoperabilidad
Tres nuevas características mejoran la compatibilidad con la interoperabilidad nativa y las bibliotecas de bajo
nivel que requieren alto rendimiento: enteros de tamaño nativo, punteros de función y la omisión de la marca
localsinit .
Los enteros de tamaño nativo, nint y nuint , son tipos enteros. Se expresan mediante los tipos subyacentes
System.IntPtr y System.UIntPtr. El compilador muestra las conversiones y operaciones adicionales para estos
tipos como enteros nativos. Los enteros con tamaño nativo definen propiedades para MaxValue o MinValue .
Estos valores no se pueden expresar como constantes en tiempo de compilación porque dependen del tamaño
nativo de un entero en el equipo de destino. Estos valores son de solo lectura en el entorno de ejecución. Puede
usar valores constantes para nint en el intervalo [ int.MinValue .. int.MaxValue ]. Puede usar valores
constantes para nuint en el intervalo [ uint.MinValue .. uint.MaxValue ]. El compilador realiza un plegamiento
constante para todos los operadores unarios y binarios que usan los tipos System.Int32 y System.UInt32. Si el
resultado no cabe en 32 bits, la operación se ejecuta en tiempo de ejecución y no se considera una constante.
Los enteros con tamaño nativo pueden aumentar el rendimiento en escenarios en los que se usa la aritmética de
enteros y es necesario tener el rendimiento más rápido posible. Para obtener más información, consulte los
tipos nint y nuint
Los punteros de función proporcionan una sintaxis sencilla para acceder a los códigos de operación de lenguaje
intermedio ldftn y calli . Puede declarar punteros de función con la nueva sintaxis de delegate* . Un tipo
delegate* es un tipo de puntero. Al invocar el tipo delegate* se usa calli , a diferencia de un delegado que
usa callvirt en el método Invoke() . Sintácticamente, las invocaciones son idénticas. La invocación del
puntero de función usa la convención de llamada managed . Agregue la palabra clave unmanaged después de la
sintaxis de delegate* para declarar que quiere la convención de llamada unmanaged . Se pueden especificar
otras convenciones de llamada mediante atributos en la declaración de delegate* . Para obtener más
información, vea Código no seguro y tipos de puntero.
Por último, puede agregar System.Runtime.CompilerServices.SkipLocalsInitAttribute para indicar al compilador
que no emita la marca localsinit . Esta marca indica al CLR que inicialice en cero todas las variables locales. La
marca localsinit ha sido el comportamiento predeterminado en C# desde la versión 1.0. Pero la inicialización
en cero adicional puede afectar al rendimiento en algunos escenarios. En concreto, cuando se usa stackalloc .
En esos casos, puede agregar SkipLocalsInitAttribute. Puede agregarlo a un único método o propiedad, a un
objeto class , struct , interface , o incluso a un módulo. Este atributo no afecta a los métodos abstract ;
afecta al código generado para la implementación. Para obtener más información, vea Atributo SkipLocalsInit .
Estas características pueden aumentar significativamente el rendimiento en algunos escenarios. Solo se deben
usar después de realizar pruebas comparativas minuciosamente antes y después de la adopción. El código que
implica enteros con tamaño nativo se debe probar en varias plataformas de destino con distintos tamaños de
enteros. Las demás características requieren código no seguro.
El tipo de destino new también se puede usar cuando es necesario crear un objeto para pasarlo como
argumento a un método. Considere un método ForecastFor() con la signatura siguiente:
Otra aplicación muy útil de esta característica es para combinarla con propiedades de solo inicialización para
inicializar un objeto nuevo:
Puede devolver una instancia creada por el constructor predeterminado mediante una declaración
return new(); .
Una característica similar mejora la resolución de tipos de destino de las expresiones condicionales. Con este
cambio, las dos expresiones no necesitan tener una conversión implícita de una a otra, pero pueden tener
conversiones implícitas a un tipo de destino. Lo más probable es que no note este cambio. Lo que observará es
que ahora funcionan algunas expresiones condicionales para las que anteriormente se necesitaban
conversiones o que no se compilaban.
A partir de C# 9.0, puede agregar el modificador static a expresiones lambda o métodos anónimos. Las
expresiones lambda estáticas son análogas a las funciones static locales: un método anónimo o una expresión
lambda estáticos no puede capturar variables locales ni el estado de la instancia. El modificador static impide
la captura accidental de otras variables.
Los tipos de valor devuelto covariantes proporcionan flexibilidad a los tipos de valor devuelto de los métodos
override. Un método override puede devolver un tipo derivado del tipo de valor devuelto del método base
invalidado. Esto puede ser útil para los registros y para otros tipos que admiten métodos de generador o
clonación virtuales.
Además, el bucle foreach reconocerá y usará un método de extensión GetEnumerator que, de otro modo,
satisface el patrón foreach . Este cambio significa que foreach es coherente con otras construcciones basadas
en patrones, como el patrón asincrónico y la desconstrucción basada en patrones. En la práctica, esto quiere
decir que puede agregar compatibilidad con foreach a cualquier tipo. Debe limitar su uso a cuando la
enumeración de un objeto tiene sentido en el diseño.
Después, puede usar descartes como parámetros para las expresiones lambda. De esta forma no tiene que
asignar un nombre al argumento y el compilador puede evitar usarlo. Use _ para cualquier argumento. Para
más información, consulte sección sobre parámetros de entrada de una expresión lambda en el artículo sobre
expresiones lambda.
Por último, ahora puede aplicar atributos a las funciones locales. Por ejemplo, puede aplicar anotaciones de
atributo que admiten un valor NULL a las funciones locales.
Al igual que la mayoría de las estructuras, el método ToString() no modifica el estado. Para indicar eso, podría
agregar el modificador readonly a la declaración de ToString() :
El cambio anterior genera una advertencia del compilador, porque ToString accede a la propiedad Distance ,
que no está marcada como readonly :
warning CS8656: Call to non-readonly member 'Point.Distance.get' from a 'readonly' member results in an
implicit copy of 'this'
El compilador le advierte cuando es necesario crear una copia defensiva. La propiedad Distance no cambia el
estado, por lo que puede corregir esta advertencia si agrega el modificador readonly a la declaración:
Tenga en cuenta que el modificador readonly es necesario en una propiedad de solo lectura. El compilador no
presupone que los descriptores de acceso get no modifican el estado; debe declarar readonly explícitamente.
Las propiedades implementadas automáticamente son una excepción; el compilador tratará todos los
captadores implementados automáticamente como readonly , por lo que aquí no es necesario agregar el
modificador readonly a las propiedades X e Y .
El compilador aplica la regla por la que los miembros readonly no modifican el estado. El método siguiente no
se compilará a menos que se quite el modificador readonly :
Esta característica permite especificar la intención del diseño para que el compilador pueda aplicarla, y realizar
optimizaciones basadas en dicha intención.
Para obtener más información, vea la sección Miembros de instancia readonly del artículo Tipos de estructura.
Si la aplicación definió un tipo RGBColor construido a partir de los componentes R , G y B , podría convertir
un valor Rainbow a sus valores RGB con el método siguiente que contiene una expresión switch:
La coincidencia de patrones crea una sintaxis concisa para expresar este algoritmo.
Para obtener más información, vea la sección Patrón de propiedad del artículo Patrones.
Patrones de tupla
Algunos algoritmos dependen de varias entradas. Los patrones de tupla permiten hacer cambios en función
de varios valores, expresados como una tupla. El código siguiente muestra una expresión switch del juego
piedra, papel, tijeras:
Los mensajes indican el ganador. El caso de descarte representa las tres combinaciones de valores equivalentes,
u otras entradas de texto.
Patrones posicionales
Algunos tipos incluyen un método Deconstruct que deconstruye sus propiedades en variables discretas.
Cuando un método Deconstruct es accesible, puede usar patrones posicionales para inspeccionar las
propiedades del objeto y usar esas propiedades en un patrón. Tenga en cuenta la siguiente clase Point que
incluye un método Deconstruct con el objetivo de crear variables discretas para X y Y :
Además, tenga en cuenta la siguiente enumeración, que representa diversas posiciones de un cuadrante:
El método siguiente usa el patrón posicional para extraer los valores de x y y . A continuación, usa una
cláusula when para determinar el valor Quadrant del punto:
static Quadrant GetQuadrant(Point point) => point switch
{
(0, 0) => Quadrant.Origin,
var (x, y) when x > 0 && y > 0 => Quadrant.One,
var (x, y) when x < 0 && y > 0 => Quadrant.Two,
var (x, y) when x < 0 && y < 0 => Quadrant.Three,
var (x, y) when x > 0 && y < 0 => Quadrant.Four,
var (_, _) => Quadrant.OnBorder,
_ => Quadrant.Unknown
};
El patrón de descarte del modificador anterior coincide cuando x o y es 0, pero no ambos. Una expresión
switch debe generar un valor o producir una excepción. Si ninguno de los casos coincide, la expresión switch
produce una excepción. El compilador genera una advertencia si no cubre todos los posibles casos en la
expresión switch.
Puede explorar las técnicas de coincidencia de patrones en este tutorial avanzado sobre la coincidencia de
patrones. Para obtener más información sobre un patrón posicional, vea la sección Patrón posicional del artículo
Patrones.
Declaraciones using
Una declaración using es una declaración de variable precedida por la palabra clave using . Indica al
compilador que la variable que se declara debe eliminarse al final del ámbito de inclusión. Por ejemplo,
considere el siguiente código que escribe un archivo de texto:
En el ejemplo anterior, el archivo se elimina cuando se alcanza la llave de cierre del método. Ese es el final del
ámbito en el que se declara file . El código anterior es equivalente al siguiente código que usa las instrucciones
using clásicas:
static int WriteLinesToFile(IEnumerable<string> lines)
{
using (var file = new System.IO.StreamWriter("WriteLines2.txt"))
{
int skippedLines = 0;
foreach (string line in lines)
{
if (!line.Contains("Second"))
{
file.WriteLine(line);
}
else
{
skippedLines++;
}
}
return skippedLines;
} // file is disposed here
}
En el ejemplo anterior, el archivo se elimina cuando se alcanza la llave de cierre asociada con la instrucción
using .
En ambos casos, el compilador genera la llamada a Dispose() . El compilador genera un error si la expresión de
la instrucción using no se puede eliminar.
int M()
{
int y;
LocalFunction();
return y;
El código siguiente contiene una función local estática. Puede ser estática porque no accede a las variables del
ámbito de inclusión:
int M()
{
int y = 5;
int x = 7;
return Add(x, y);
Secuencias asincrónicas
A partir de C# 8.0, puede crear y consumir secuencias de forma asincrónica. Un método que devuelve una
secuencia asincrónica tiene tres propiedades:
1. Se declara con el modificador async .
2. Devuelve IAsyncEnumerable<T>.
3. El método contiene instrucciones yield return que devuelven elementos sucesivos de la secuencia
asincrónica.
Para consumir una secuencia asincrónica es necesario agregar la palabra clave await delante de la palabra
clave foreach al enumerar los elementos de la secuencia. Para agregar la palabra clave await es necesario
declarar el método que enumera la secuencia asincrónica con el modificador async y devolver un tipo
permitido para un método async . Normalmente, esto significa devolver Task o Task<TResult>. También puede
ser ValueTask o ValueTask<TResult>. Un método puede consumir y producir una secuencia asincrónica, lo que
significa que devolvería IAsyncEnumerable<T>. El siguiente código genera una secuencia de 0 a 19, con una
espera de 100 ms entre la generación de cada número:
public static async System.Collections.Generic.IAsyncEnumerable<int> GenerateSequence()
{
for (int i = 0; i < 20; i++)
{
await Task.Delay(100);
yield return i;
}
}
Puede probar secuencias asincrónicas por su cuenta en nuestro tutorial sobre la creación y consumo de
secuencias asincrónicas. Los elementos de secuencia se procesan de forma predeterminada en el contexto
capturado. Si quiere deshabilitar la captura del contexto, use el método de extensión
TaskAsyncEnumerableExtensions.ConfigureAwait. Para obtener más información sobre los contextos de
sincronización y la captura del contexto actual, vea el artículo sobre el consumo del patrón asincrónico basado
en tareas.
Asincrónica descartable
A partir C# 8.0, el lenguaje admite tipos descartables asincrónicos que implementan la
interfaz System.IAsyncDisposable. Use la instrucción await using para trabajar con un objeto descartable de
forma asincrónica. Para obtener más información, vea el artículo Implementación de un método DisposeAsync.
Índices y rangos
Los índices y rangos proporcionan una sintaxis concisa para acceder a elementos únicos o intervalos en una
secuencia.
Esta compatibilidad con lenguajes se basa en dos nuevos tipos y dos nuevos operadores:
System.Index representa un índice en una secuencia.
El índice desde el operador final ^ , que especifica que un índice es relativo al final de la secuencia.
System.Range representa un subrango de una secuencia.
El operador de intervalo .. , que especifica el inicio y el final de un intervalo como sus operandos.
Comencemos con las reglas de índices. Considere un elemento sequence de matriz. El índice 0 es igual que
sequence[0] . El índice ^0 es igual que sequence[sequence.Length] . Tenga en cuenta que sequence[^0] produce
una excepción, al igual que sequence[sequence.Length] . Para cualquier número n , el índice ^n es igual que
sequence.Length - n .
Un rango especifica el inicio y el final de un intervalo. El inicio del rango es inclusivo, pero su final es exclusivo,
lo que significa que el inicio se incluye en el rango, pero el final no. El rango [0..^0] representa todo el
intervalo, al igual que [0..sequence.Length] representa todo el intervalo.
Veamos algunos ejemplos. Tenga en cuenta la siguiente matriz, anotada con su índice desde el principio y desde
el final:
var words = new string[]
{
// index from start index from end
"The", // 0 ^9
"quick", // 1 ^8
"brown", // 2 ^7
"fox", // 3 ^6
"jumped", // 4 ^5
"over", // 5 ^4
"the", // 6 ^3
"lazy", // 7 ^2
"dog" // 8 ^1
}; // 9 (or words.Length) ^0
El siguiente código crea un subrango con las palabras "quick", "brown" y "fox". Va de words[1] a words[3] . El
elemento words[4] no se encuentra en el intervalo.
El siguiente código crea un subrango con "lazy" y "dog". Incluye words[^2] y words[^1] . El índice del final
words[^0] no se incluye:
En los ejemplos siguientes se crean rangos con final abierto para el inicio, el final o ambos:
No solo las matrices admiten índices y rangos. También puede usar índices y rangos con string, Span<T> o
ReadOnlySpan<T>. Para más información, consulte Compatibilidad con tipos para los índices y los rangos.
Puede explorar más información acerca de los índices y los intervalos en el tutorial sobre índices e intervalos.
el tipo Coords<int> es un tipo no administrado en C# 8.0 y versiones posteriores. Al igual que en el caso de
cualquier tipo no administrado, puede crear un puntero a una variable de este tipo o asignar un bloque de
memoria en la pila para las instancias de este tipo:
Entre C# 7.0 y C# 7.3 se han incorporado varias características y mejoras incrementales en la experiencia de
desarrollo con C#. En este artículo se proporciona información general sobre las nuevas características y
opciones del compilador del lenguaje. Se describe el comportamiento para C# 7.3, que es la versión más
reciente compatible con las aplicaciones basadas en .NET Framework.
El elemento de configuración de selección de versión del lenguaje se agregó con C# 7.1, lo que permite
especificar la versión de lenguaje del compilador en el archivo del proyecto.
En C# 7.0-7.3 se agregan estas características y temas al lenguaje C#:
Tuplas y descartes
Puede crear tipos ligeros sin nombre que contengan varios campos públicos. Los compiladores y las
herramientas IDE comprenden la semántica de estos tipos.
Los descartes son variables temporales y de solo escritura que se usan en argumentos cuando el valor
asignado es indiferente. Son especialmente útiles al deconstruir tuplas y tipos definidos por el usuario,
así como al realizar llamadas a métodos con parámetros out .
Coincidencia de patrones
Puede crear la lógica de bifurcación en función de tipos y valores arbitrarios de los miembros de esos
tipos.
Método async Main
El punto de entrada de una aplicación puede tener el modificador async .
Funciones locales
Puede anidar funciones en otras funciones para limitar su ámbito y visibilidad.
Más miembros con forma de expresión
La lista de miembros que se pueden crear con expresiones ha crecido.
Expresiones throw
Puede iniciar excepciones en construcciones de código que antes no se permitían porque throw era
una instrucción.
Expresiones literales default
Se pueden usar expresiones literales predeterminadas en expresiones de valor predeterminadas
cuando el tipo de destino se pueda inferir.
Mejoras en la sintaxis de literales numéricos
Nuevos tokens mejoran la legibilidad de las constantes numéricas.
Variables de out
Puede declarar valores out insertados como argumentos en el método cuando se usen.
Argumentos con nombre no finales
Los argumentos con nombre pueden ir seguidos de argumentos posicionales.
Modificador de acceso private protected
El modificador de acceso private protected permite el acceso de clases derivadas en el mismo
ensamblado.
Mejoras en la resolución de sobrecarga
Nuevas reglas para resolver la ambigüedad de la resolución de sobrecarga.
Técnicas para escribir código eficiente seguro
Una combinación de mejoras en la sintaxis que permiten trabajar con tipos de valor mediante la
semántica de referencia.
Por último, el compilador tiene nuevas opciones:
-refout y para controlar la generación de ensamblados de referencia.
-refonly
-publicsign para habilitar la firma de ensamblados de software de código abierto (OSS).
-pathmap para proporcionar una asignación para los directorios de origen.
En el resto de este artículo se proporciona información general sobre cada característica. Para cada
característica, obtendrá información sobre el razonamiento subyacente y la sintaxis. Puede explorar estas
características en su entorno mediante la herramienta global dotnet try :
1. Instale la herramienta global dotnet-try.
2. Clone el repositorio dotnet/try-samples.
3. Establezca el directorio actual en el subdirectorio csharp7 para el repositorio try-samples.
4. Ejecute dotnet try .
Tuplas y descartes
C# ofrece una sintaxis enriquecida para clases y estructuras que se usa para explicar la intención del diseño. Pero
a veces esa sintaxis enriquecida requiere trabajo adicional con apenas beneficio. Puede que a menudo escriba
métodos que requieren una estructura simple que contenga más de un elemento de datos. Para admitir estos
escenarios, se han agregado tuplas a C#. Las tuplas son estructuras de datos ligeros que contienen varios
campos para representar los miembros de datos. Los campos no se validan y no se pueden definir métodos
propios. Los tipos de tupla de C# admiten == y != . Para obtener más información,
NOTE
Las tuplas estaban disponibles antes de C# 7.0, pero no eran eficientes ni compatibles con ningún lenguaje. Esto
significaba que solo se podía hacer referencia a los elementos tupla como Item1 , Item2 , por ejemplo. C# 7.0 presenta
la compatibilidad de lenguaje con las tuplas, que permite usar nombres semánticos en los campos de una tupla mediante
tipos de tupla nuevos y más eficientes.
Puede crear una tupla asignando un valor a cada miembro, y, opcionalmente, proporcionando nombres
semánticos a cada uno de los miembros de la tupla:
La tupla namedLetters contiene campos denominados Alpha y Beta . Esos nombres solo existen en tiempo de
compilación y no se conservan, por ejemplo, al inspeccionar la tupla mediante la reflexión en tiempo de
ejecución.
En la asignación de una tupla, también pueden especificarse los nombres de los campos a la derecha de la
asignación:
Puede que a veces quiera desempaquetar los miembros de una tupla devueltos de un método. Para ello, declare
distintas variables para cada uno de los valores de la tupla. Este desempaquetado se denomina deconstrucción
de la tupla:
(int max, int min) = Range(numbers);
Console.WriteLine(max);
Console.WriteLine(min);
También puede proporcionar una deconstrucción similar para cualquier tipo de .NET. Un método Deconstruct se
escribe como un miembro de la clase. Ese método Deconstruct proporciona un conjunto de argumentos out
para cada una de las propiedades que quiere extraer. Tenga en cuenta que esta clase Point proporciona un
método deconstructor que extrae las coordenadas X e Y :
Muchas veces, cuando se inicializa una tupla, las variables usadas en el lado derecho de la asignación son las
mismas que los nombres que querríamos dar a los elementos de tupla: Los nombres de los elementos de tupla
se pueden deducir de las variables usadas para inicializar la tupla:
int count = 5;
string label = "Colors used in the map";
var pair = (count, label); // element names are "count" and "label"
Encontrará más información sobre esta característica en el artículo sobre tipos de tuplas.
Habitualmente, al deconstruir una tupla o realizar una llamada a un método mediante parámetros out , debe
definir una variable con un valor indiferente y que no vaya a usar. C# agrega la compatibilidad con descartes
para gestionar este tipo de escenarios. Un descarte es una variable de solo escritura con el nombre _ (el
carácter de guion bajo). Puede asignar todos los valores que quiera descartar a una única variable. Un descarte
se parece a una variable no asignada, aunque no puede usarse en el código (excepto la instrucción de
asignación).
Los descartes se admiten en los escenarios siguientes:
Al deconstruir tuplas o tipos definidos por el usuario.
Al realizar llamadas a métodos mediante parámetros out.
En una operación de coincidencia de patrones con el operador is y la instrucción switch .
Como un identificador independiente cuando quiera identificar explícitamente el valor de una asignación
como descarte.
En el ejemplo siguiente se define un método QueryCityData que devuelve una tupla de tipo 3 con datos de una
ciudad correspondientes a dos años diferentes. La llamada de método del ejemplo se refiere únicamente a los
dos valores de rellenado que devuelve el método, por lo que trata los valores restantes de la tupla como
descartes al deconstruirla.
using System;
private static (string name, int pop, double size) QueryCityData(string name)
{
if (name == "New York City")
return (name, 8175133, 468.48);
Detección de patrones
La coincidencia de patrones es un conjunto de características que permiten nuevas formas de expresar el flujo
de control en el código. En las variables, puede probar su tipo, sus valores o los valores de sus propiedades.
Estas técnicas crean un flujo de código más legible.
La coincidencia de patrones admite expresiones is y switch . Cada una de ellas habilita la inspección de un
objeto y sus propiedades para determinar si el objeto cumple el patrón buscado. Use la palabra clave when para
especificar reglas adicionales para el patrón.
La expresión de patrón is extiende el conocido operador is para consultar el tipo de un objeto y asignar el
resultado en una instrucción. En el código siguiente se comprueba si una variable es de tipo int y, si lo es, se
agrega a la suma actual:
En el pequeño ejemplo anterior se muestran las mejoras en la expresión is . Puede probar con tipos de valor y
tipos de referencia, y asignar el resultado correcto a una nueva variable del tipo correcto.
La expresión de coincidencia switch tiene una sintaxis conocida, basada en la instrucción switch que ya forma
parte del lenguaje C#. La instrucción switch actualizada tiene varias construcciones nuevas:
El tipo de control de una expresión switch ya no se limita a tipos enteros, tipos Enum , string o a un tipo
que acepta valores NULL correspondiente a uno de esos tipos. Se puede usar cualquier tipo.
Puede probar el tipo de la expresión switch en todas las etiquetas case . Como sucede con la expresión is
, puede asignar una variable nueva a ese tipo.
Puede agregar una cláusula when para probar más condiciones en esa variable.
Ahora el orden de las etiquetas case es importante. Se ejecuta la primera rama de la que se quiere obtener
la coincidencia, mientras que el resto se omite.
En el código siguiente se muestran estas características:
public static int SumPositiveNumbers(IEnumerable<object> sequence)
{
int sum = 0;
foreach (var i in sequence)
{
switch (i)
{
case 0:
break;
case IEnumerable<int> childSequence:
{
foreach(var item in childSequence)
sum += (item > 0) ? item : 0;
break;
}
case int n when n > 0:
sum += n;
break;
case null:
throw new NullReferenceException("Null found in sequence");
default:
throw new InvalidOperationException("Unrecognized type");
}
}
return sum;
}
A partir de C# 7.1, la expresión de patrón para is y el patrón de tipo switch pueden tener el tipo de un
parámetro de tipo genérico. Esto puede ser especialmente útil al comprobar los tipos que pueden ser tipos
struct o class y si quiere evitar la conversión boxing.
Puede obtener más información sobre la coincidencia de patrones en Coincidencia de patrones en C#.
Async main
Un método async main permite usar await en el método Main . Anteriormente, hubiera sido necesario escribir
lo siguiente:
En el artículo sobre async main de la guía de programación puede leer más detalles al respecto.
Funciones locales
Muchos diseños de clases incluyen métodos que se llaman desde una sola ubicación. Estos métodos privados
adicionales mantienen cada método pequeño y centrado. Las funciones locales permiten declarar métodos en el
contexto de otro método. Las funciones locales facilitan que los lectores de la clase vean que el método local
solo se llama desde el contexto en el que se declara.
Hay dos casos de uso comunes para las funciones locales: métodos de iterador públicos y métodos asincrónicos
públicos. Ambos tipos de métodos generan código que informa de errores más tarde de lo que los
programadores podrían esperar. En los métodos de iterador, las excepciones solo se observan al llamar a código
que enumera la secuencia devuelta. En los métodos asincrónicos, las excepciones solo se observan cuando se
espera al elemento Task devuelto. En el ejemplo siguiente se muestra la separación de la validación de
parámetros de la implementación de iteradores mediante una función local:
return alphabetSubsetImplementation();
IEnumerable<char> alphabetSubsetImplementation()
{
for (var c = start; c < end; c++)
yield return c;
}
}
La misma técnica se puede emplear con métodos async para asegurarse de que las excepciones derivadas de la
validación de argumentos se inician antes de comenzar el trabajo asincrónico:
public Task<string> PerformLongRunningWork(string address, int index, string name)
{
if (string.IsNullOrWhiteSpace(address))
throw new ArgumentException(message: "An address is required", paramName: nameof(address));
if (index < 0)
throw new ArgumentOutOfRangeException(paramName: nameof(index), message: "The index must be non-
negative");
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException(message: "You must supply a name", paramName: nameof(name));
return longRunningWorkImplementation();
[field: SomeThingAboutFieldAttribute]
public int SomeProperty { get; set; }
NOTE
Algunos de los diseños que se admiten con funciones locales también se pueden realizar con expresiones lambda. Para
más información, consulte Funciones locales frente a expresiones lambda.
// Expression-bodied constructor
public ExpressionMembersExample(string label) => this.Label = label;
// Expression-bodied finalizer
~ExpressionMembersExample() => Console.Error.WriteLine("Finalized!");
Estas nuevas ubicaciones para los miembros con forma de expresión representan un hito importante para el
lenguaje C#: miembros de la comunidad que trabajan en el proyecto Roslyn de código abierto implementaron
estas características.
Cambiar un método a un miembro con cuerpo de expresión es un cambio compatible con un elemento binario.
Expresiones throw
En C#, throw siempre ha sido una instrucción. Como throw es una instrucción, no una expresión, había
construcciones de C# en las que no se podía usar. Incluyen expresiones condicionales, expresiones de fusión
nulas y algunas expresiones lambda. La incorporación de miembros con forma de expresión agrega más
ubicaciones donde las expresiones throw resultarían útiles. Para que pueda escribir cualquiera de estas
construcciones, C# 7.0 presenta las expresiones throw .
Esta adición facilita la escritura de código más basado en expresiones. No se necesitan instrucciones adicionales
para la comprobación de errores.
Ahora, se puede pasar por alto el tipo del lado derecho de la inicialización:
Para más información, consulte la sección Literal default del artículo Operador default.
El 0b al principio de la constante indica que el número está escrito como número binario. Los números binarios
pueden ser muy largos, por lo que a menudo resulta más fácil ver los patrones de bits si se introduce _ como
separador de dígitos, como se ha mostrado en la constante binaria del ejemplo anterior. El separador de dígitos
puede aparecer en cualquier parte de la constante. En números de base 10, es habitual usarlo como separador
de miles. Los literales numéricos hexadecimales y binarios pueden empezar con _ :
El separador de dígitos también se puede usar con tipos decimal , float y double :
Variables out
La sintaxis existente que admite parámetros out se ha mejorado en C# 7. Ahora puede declarar variables out
en la lista de argumentos de una llamada a método, en lugar de escribir una instrucción de declaración distinta:
Para mayor claridad, es posible que quiera especificar el tipo de la variable out , como se ha mostrado en el
ejemplo anterior. Pero el lenguaje admite el uso de una variable local con tipo implícito:
public class D : B
{
public D(int i) : base(i, out var j)
{
Console.WriteLine($"The value of 'j' is {j}");
}
}
Puede declarar el valor devuelto como un elemento ref y modificar ese valor en la matriz, como se muestra en
el código siguiente:
Para obtener más información, vea el artículo sobre valores devueltos de ref y ref locales, así como el
artículo sobre foreach .
Para más información, consulte el artículo sobre la palabra clave ref.
Expresiones ref condicionales
Por último, la expresión condicional puede producir un resultado de referencia en lugar de un resultado de valor.
Por ejemplo, podría escribir lo siguiente para recuperar una referencia al primer elemento en una de dos
matrices:
La sobrecarga por valor (la primera del ejemplo anterior) es mejor que la de la versión de referencia de solo
lectura. Para llamar a la versión con el argumento de referencia de solo lectura, debe incluir el modificador in
cuando llame al método.
Para obtener más información, consulte el artículo sobre el modificador de parámetros in .
Hay más tipos compatibles con la instrucción fixed
La instrucción fixed admite un conjunto limitado de tipos. A partir de C# 7.3, cualquier tipo que contenga un
método GetPinnableReference() que devuelve un ref T o ref readonly T puede ser fixed . La adición de esta
característica significa que fixed puede utilizarse con System.Span<T> y tipos relacionados.
Para más información, vea el artículo sobre la instrucción fixed en la referencia del lenguaje.
Indexación de campos fixed sin requerir anclaje
Considere esta estructura:
unsafe struct S
{
public fixed int myFixedField[10];
}
En versiones anteriores de C#, era necesario anclar una variable para acceder a uno de los enteros que forman
parte de myFixedField . Ahora, el código siguiente se compila sin anclar la variable p dentro de una instrucción
fixed independiente:
class C
{
static S s = new S();
La variable p tiene acceso a un elemento en myFixedField . No es necesario declarar otra variable int*
independiente. Todavía necesita un contexto unsafe . En versiones anteriores de C#, es necesario declarar un
segundo puntero fijo:
class C
{
static S s = new S();
Ahora, esa misma sintaxis se puede aplicar a las matrices que se declaran con stackalloc :
NOTE
Para poder usar el tipo ValueTask<TResult> tiene que agregar el paquete NuGet System.Threading.Tasks.Extensions .
Esta mejora es especialmente útil para que los creadores de bibliotecas eviten la asignación de un elemento
Task en código de rendimiento crítico.
El equipo de Roslyn mantiene una lista de cambios importantes en los compiladores de C# y Visual Basic. Puede
encontrar información sobre esos cambios en estos vínculos del repositorio de GitHub:
Cambios importantes en Roslyn después de .NET 5
Cambios importantes en la versión 16.8 de VS2019 incorporados con .NET 5.0 y C# 9.0
Cambios importantes en VS2019 Update 1 y versiones posteriores en comparación con VS2019
Cambios importantes desde VS2017 (C# 7)
Cambios importantes en Roslyn 3.0 (VS2019) desde Roslyn 2.* (VS2017)
Cambios importantes en Roslyn 2.0 (VS2017) desde Roslyn 1.* (VS2015) y en el compilador nativo de C#
(VS2013 y anterior).
Cambios importantes en Roslyn 1.0 (VS2015) desde el compilador nativo de C# (VS2013 y anterior).
Cambio de versión de Unicode en C# 6
Historia de C#
16/09/2021 • 14 minutes to read
En este artículo se proporciona un historial de cada versión principal del lenguaje C#. El equipo de C# continúa
innovando y agregando nuevas características. Se puede encontrar información sobre el estado detallado de las
características de lenguaje, incluidas las características consideradas para las próximas versiones, en el
repositorio dotnet/roslyn de GitHub.
IMPORTANT
El lenguaje C# se basa en tipos y métodos en lo que la especificación de C# define como una biblioteca estándar para
algunas de las características. La plataforma .NET ofrece los tipos y métodos en un número de paquetes. Un ejemplo es el
procesamiento de excepciones. Cada expresión o instrucción throw se comprueba para asegurarse de que el objeto que
se genera deriva de Exception. Del mismo modo, cada catch se comprueba para asegurarse de que el tipo que se
captura deriva de Exception. Cada versión puede agregar requisitos nuevos. Para usar las características más recientes del
lenguaje en entornos anteriores, es posible que tenga que instalar bibliotecas específicas. Estas dependencias están
documentadas en la página de cada versión específica. Puede obtener más información sobre las relaciones entre lenguaje
y biblioteca para tener más antecedentes sobre esta dependencia.
C# versión 1.0
Si echa la vista atrás, la versión 1.0 de C#, publicada con Visual Studio .NET 2002, se parecía mucho a Java.
Como parte de sus objetivos de diseño indicados para ECMA, intentaba ser un "lenguaje orientado a objetos
que fuera sencillo, moderno y para fines generales". En aquel momento, parecerse a Java significaba que
conseguía esos primeros objetivos de diseño.
Pero si volvemos a echarle un vistazo a C# 1.0 ahora, no lo verá tan claro. Carecía de capacidades asincrónicas
integradas y de algunas funcionalidades útiles de genéricos que se dan por sentado. De hecho, carecía por
completo de genéricos. ¿Y LINQ? Aún no estaba disponible. Esas características tardarían unos años más en
agregarse.
C# 1.0 parecía estar privado de características, en comparación con la actualidad. Lo normal era tener que
escribir código detallado. Pero aun así, hay que empezar por algo. C# 1.0 era una alternativa viable a Java en la
plataforma Windows.
Las principales características de C# 1.0 incluían lo siguiente:
Clases
Structs
Interfaces
Eventos
Propiedades
Delegados
Operadores y expresiones
Instrucciones
Atributos
Versión 1.2 de C#
Versión 1.2 de C# incluida en Visual Studio .NET 2003. Contenía algunas pequeñas mejoras del lenguaje. Lo más
notable es que, a partir de esa versión, el código se generaba en un bucle foreach llamado Dispose en un
IEnumerator cuando ese IEnumerator implementaba IDisposable.
C# versión 2.0
Aquí las cosas empiezan a ponerse interesantes. Echemos un vistazo a algunas de las principales características
de C# 2.0, que se publicó en 2005 junto con Visual Studio 2005:
Genéricos
Tipos parciales
Métodos anónimos
Tipos de valores que aceptan valores NULL
Iteradores
Covarianza y contravarianza
Otras características de C# 2.0 agregaron capacidades a las características existentes:
Accesibilidad independiente de captador o establecedor
Conversiones de grupos de métodos (delegados)
Clases estáticas
Inferencia de delegados
Aunque puede que C# haya comenzado como un lenguaje genérico orientado a objetos, la versión 2.0 de C#
cambió esto enseguida. En cuanto se pusieron con ella, se centraron en algunos puntos problemáticos graves
para los desarrolladores. Y lo hicieron a lo grande.
Con los genéricos, los tipos y métodos pueden operar en un tipo arbitrario a la vez que conservan la seguridad
de tipos. Por ejemplo, tener List<T> nos permite tener List<string> o List<int> y realizar operaciones de tipo
seguro en esas cadenas o en enteros mientras los recorremos en iteración. Es mejor usar genéricos que crear un
tipo ListInt que derive de ArrayList , o que convertir desde Object en cada operación.
C# 2.0 incorporó los iteradores. Para explicarlo brevemente, los iteradores permiten examinar todos los
elementos de List (u otros tipos enumerables) con un bucle de foreach . Tener iteradores como una parte de
primera clase del lenguaje mejoró drásticamente la facilidad de lectura del lenguaje y la capacidad de las
personas de razonar sobre el código.
Aun así, C# seguía yendo por detrás de Java. Java ya había publicado versiones que incluían genéricos e
iteradores. Pero esto cambiaría pronto a medida que los idiomas siguieran evolucionando.
C# versión 3.0
La versión 3.0 de C# llegó a finales de 2007, junto con Visual Studio 2008, aunque la cartera completa de
características de lenguaje no llegaría realmente hasta la versión 3.5 de .NET Framework. Esta versión marcó un
cambio importante en el crecimiento de C#. Estableció C# como un lenguaje de programación realmente
formidable. Echemos un vistazo a algunas de las principales características de esta versión:
Propiedades implementadas automáticamente
Tipos anónimos (Guía de programación de C#).
Expresiones de consulta
Expresiones lambda
Árboles de expresión
Métodos de extensión
Variables locales con asignación implícita de tipos
Métodos parciales
Inicializadores de objeto y colección
En retrospectiva, muchas de estas características parecen inevitables e indivisibles. Todas ellas encajan
estratégicamente. Por lo general se considera que la mejor característica de la versión de C# fue la expresión de
consulta, también conocida como Language-Integrated Query (LINQ).
Una vista más matizada examina árboles de expresión, expresiones lambda y tipos anónimos como la base
sobre la que se construye LINQ. Sin embargo, en cualquier caso, C# 3.0 presentó un concepto revolucionario. C#
3.0 había comenzado a sentar las bases para convertir C# en un lenguaje híbrido funcional y orientado a
objetos.
En concreto, permitía escribir consultas declarativas en estilo de SQL para realizar operaciones en colecciones,
entre otras cosas. En lugar de escribir un bucle de for para calcular el promedio de una lista de enteros,
permitía hacerlo fácilmente como list.Average() . La combinación de métodos de extensión y expresiones de
consulta hizo que esa lista de enteros pareciera haberse vuelto más inteligente.
Llevó tiempo hasta que los usuarios realmente captaron e integraron el concepto, pero ocurrió gradualmente.
Ahora, años más tarde, el código es mucho más conciso, sencillo y funcional.
C# versión 4.0
La versión 4.0 de C#, publicada con Visual Studio 2010, tuvo que lidiar con el carácter innovador que había
adquirido la versión 3.0. Con la versión 3.0, el lenguaje de C# dejó de estar a la sombra de Java y alcanzó una
posición prominente. El lenguaje se estaba convirtiendo rápidamente en algo elegante.
La siguiente versión introdujo algunas nuevas características interesantes:
Enlace dinámico
Argumentos opcionales/con nombre
Covariante y contravariante de genéricos
Tipos de interoperabilidad insertados
Los tipos de interoperabilidad incrustados facilitaron el problema de implementación de crear ensamblados de
interoperabilidad COM para la aplicación. La covarianza y contravarianza de genéricos proporcionan más
capacidad para usar genéricos, pero son más bien académicos y probablemente más valorados por autores de
bibliotecas y Framework. Los parámetros opcionales y con nombre permiten eliminar muchas sobrecargas de
métodos y proporcionan mayor comodidad. Pero ninguna de esas características está modificando el paradigma
exactamente.
La característica más importante fue la introducción de la palabra clave dynamic . Con la palabra clave dynamic ,
en la versión 4.0 de C# se introdujo la capacidad de invalidar el compilador durante la escritura en tiempo de
compilación. Al usar la palabra clave dinámica, puede crear constructos similares a los lenguajes tipados
dinámicamente, como JavaScript. Puede crear dynamic x = "a string" y luego agregarle seis, dejando que el
runtime decida qué debería suceder después.
Los enlaces dinámicos pueden dar lugar a errores, pero también otorgan un gran poder sobre el lenguaje.
C# versión 5.0
La versión 5.0 de C#, publicada con Visual Studio 2012, era una versión centrada del lenguaje. Casi todo el
trabajo de esa versión se centró en otro concepto de lenguaje innovador: el modelo async y await para la
programación asincrónica. Estas son las principales características:
Miembros asincrónicos
Atributos de información del llamador
Vea también
Proyecto de código: Atributos de información del autor de llamada en C# 5.0
El atributo de información del autor de la llamada permite recuperar fácilmente información sobre el contexto
donde se está ejecutando sin tener que recurrir a una gran cantidad de código de reflexión reutilizable. Tiene
muchos usos en tareas de registro y diagnóstico.
Pero async y await son los auténticos protagonistas de esta versión. Cuando estas características salieron a la
luz en 2012, C# cambió de nuevo las reglas del juego al integrar la asincronía en el lenguaje como un
participante de primera clase. Si alguna vez ha trabajado con operaciones de larga duración y la
implementación de sitios web de devoluciones de llamada, probablemente le haya encantado esta característica
del lenguaje.
C# versión 6.0
Con las versiones 3.0 y 5.0, C# había agregado nuevas características destacables a un lenguaje orientado a
objetos. Con la versión 6.0, publicada con Visual Studio 2015, en lugar de introducir una característica
innovadora y predominante, se publicaron muchas características menores que aumentaron la productividad de
la programación de C#. Estas son algunas de ellas:
Importaciones estáticas
Filtros de excepciones
Inicializadores de propiedades automáticas
Miembros de cuerpo de expresión
Propagador de null
Interpolación de cadenas
operador nameof
Entre las otras características nuevas se incluyen estas:
Inicializadores de índice
Await en bloques catch y finally
Valores predeterminados para las propiedades solo de captador
Cada una de estas características es interesante en sí misma. Pero si las observamos en su conjunto, vemos un
patrón interesante. En esta versión, C# eliminó lenguaje reutilizable para que el código fuera más fluido y fácil
de leer. Así que, para los que adoran el código simple y limpio, esta versión del lenguaje fue una gran
aportación.
En esta versión también se hizo otra cosa, aunque no es una característica de lenguaje tradicional: publicaron el
compilador Roslyn como un servicio. Ahora, el compilador de C# está escrito en C# y puede usarlo como parte
de su trabajo de programación.
C# versión 7.0
C# versión 7.0 se comercializó con Visual Studio 2017. Esta versión tiene algunas cosas interesantes y evolutivas
en la misma línea que C# 6.0, pero sin el compilador como servicio. Estas son algunas de las nuevas
características:
Variables out
Tuplas y deconstrucción
Coincidencia de patrones
Funciones locales
Miembros con forma de expresión expandidos
Devoluciones y variables locales ref
Otras características incluidas:
Descartes
Literales binarios y separadores de dígitos
Expresiones throw
Todas estas características ofrecen capacidades nuevas e interesantes para los desarrolladores y la posibilidad
de escribir un código de manera más clara que nunca. De manera destacada, condensan la declaración de
variables que se van a usar con la palabra clave out y permiten varios valores devueltos a través de tuplas.
Pero C# se está usando cada vez más. .NET Core ahora tiene como destino cualquier sistema operativo y tiene
puesta la mirada en la nube y la portabilidad. Por supuesto, esas nuevas capacidades ocupan las ideas y el
tiempo de los diseñadores de lenguaje, además de ofrecer nuevas características.
C# versión 7.1
C# empezó a publicar versiones de punto con C# 7.1. Esta versión agregó el elemento de configuración de
selección de versión de lenguaje, tres nuevas características de lenguaje y un nuevo comportamiento del
compilador.
Las nuevas características de lenguaje de esta versión son las siguientes:
Método async Main
El punto de entrada de una aplicación puede tener el modificador async .
Expresiones literales default
Se pueden usar expresiones literales predeterminadas en expresiones de valor predeterminadas
cuando el tipo de destino se pueda inferir.
Nombres de elementos de tupla inferidos
En muchos casos, los nombres de elementos de tupla se pueden deducir de la inicialización de la
tupla.
Coincidencia de patrones en parámetros de tipo genérico
Puede usar expresiones de coincidencia de patrones en variables cuyo tipo es un parámetro de tipo
genérico.
Por último, el compilador tiene dos opciones, -refout y -refonly , que controlan la generación de
ensamblados de referencia.
C# versión 7.2
C#7.2 agregó varias características de lenguaje pequeñas:
Técnicas para escribir código eficiente seguro
Una combinación de mejoras en la sintaxis que permiten trabajar con tipos de valor mediante la
semántica de referencia.
Argumentos con nombre no finales
Los argumentos con nombre pueden ir seguidos de argumentos posicionales.
Caracteres de subrayado iniciales en literales numéricos
Los literales numéricos ahora pueden tener caracteres de subrayado iniciales antes de los dígitos
impresos.
Modificador de acceso private protected
El modificador de acceso private protected permite el acceso de clases derivadas en el mismo
ensamblado.
Expresiones ref condicionales
El resultado de una expresión condicional ( ?: ) ahora puede ser una referencia.
C# versión 7.3
Hay dos temas principales para la versión C# 7.3. Un tema proporciona características que permiten al código
seguro ser tan eficaz como el código no seguro. El segundo tema proporciona mejoras incrementales en las
características existentes. Además, se han agregado nuevas opciones de compilador en esta versión.
Las siguientes características nuevas admiten el tema del mejor rendimiento para código seguro:
Puede acceder a campos fijos sin anclar.
Puede reasignar variables locales ref .
Puede usar inicializadores en matrices stackalloc .
Puede usar instrucciones fixed con cualquier tipo que admita un patrón.
Puede usar restricciones más genéricas.
Se hicieron las mejoras siguientes a las características existentes:
Puede probar == y != con los tipos de tupla.
Puede usar variables de expresión en más ubicaciones.
Puede asociar atributos al campo de respaldo de las propiedades autoimplementadas.
Se ha mejorado la resolución de métodos cuando los argumentos difieren por in .
Ahora, la resolución de sobrecarga tiene menos casos ambiguos.
Las nuevas opciones del compilador son:
-publicsign para habilitar la firma de ensamblados de software de código abierto (OSS).
-pathmap para proporcionar una asignación para los directorios de origen.
C# versión 8.0
C# 8.0 es la primera versión C# principal que tiene como destino específicamente .NET Core. Algunas
características se basan en nuevas funcionalidades de CLR, otras en tipos de biblioteca agregados solo a .NET
Core. C# 8.0 agrega las siguientes características y mejoras al lenguaje C#:
Miembros de solo lectura
Métodos de interfaz predeterminados
Mejoras de coincidencia de patrones:
Expresiones switch
Patrones de propiedades
Patrones de tupla
Patrones posicionales
Declaraciones using
Funciones locales estáticas
Estructuras ref descartables
Tipos de referencia que aceptan valores null
Secuencias asincrónicas
Índices y rangos
Asignación de uso combinado de NULL
Tipos construidos no administrados
Stackalloc en expresiones anidadas
Mejora de las cadenas textuales interpoladas
Los miembros de interfaz predeterminados requieren mejoras en CLR. Estas características se agregaron en CLR
para .NET Core 3.0. Los intervalos y los índices, y los flujos asincrónicos requieren nuevos tipos en las
bibliotecas de .NET Core 3.0. Los tipos de referencia que aceptan valores NULL, aunque se implementan en el
compilador, son mucho más útiles cuando se anotan bibliotecas para proporcionar información semántica
relativa al estado NULL de los argumentos y los valores devueltos. Esas anotaciones se agregan a las bibliotecas
de .NET Core.
C# versión 9.0
C# 9.0 se publicó con .NET 5. Es la versión de lenguaje predeterminada para cualquier ensamblado que tenga
como destino la versión de .NET 5. Contiene las siguientes características nuevas y mejoradas:
C# 9.0 agrega las siguientes características y mejoras al lenguaje C#:
Registros
Establecedores de solo inicialización
Instrucciones de nivel superior
Mejoras de coincidencia de patrones
Rendimiento e interoperabilidad
Enteros con tamaño nativos
Punteros de función
Supresión de la emisión de la marca localsinit
Características de ajuste y finalización
Expresiones new con tipo de destino
Funciones anónimas static
Expresiones condicionales con tipo de destino
Tipos de valor devueltos de covariante
Compatibilidad con extensiones GetEnumerator para bucles foreach
Parámetros de descarte lambda
Atributos en funciones locales
Compatibilidad con generadores de código
Inicializadores de módulo
Nuevas características para métodos parciales
C# 9.0 continúa tres de los temas de versiones anteriores: quitar complejidad, separar datos de algoritmos y
proporcionar más patrones en más lugares.
Las instrucciones de nivel superior hacen que el programa principal sea más fácil de leer. La complejidad es
innecesaria: un espacio de nombres, una clase Program y static void Main() son innecesarios.
La introducción de records proporciona una sintaxis concisa para los tipos de referencia que siguen la
semántica del valor para la igualdad. Usará estos tipos para definir contenedores de datos que normalmente
definen un comportamiento mínimo. Los establecedores de solo inicialización proporcionan la funcionalidad
para la mutación no destructiva with (expresiones) en los registros. C# 9.0 también agrega tipos de valor
devuelto de covariante para que los registros derivados puedan invalidar los métodos virtuales y devolver un
tipo derivado del tipo de valor devuelto del método base.
Las funcionalidades de coincidencia de patrones se han expandido de varias maneras. Los tipos numéricos
ahora admiten patrones de rango. Los patrones se pueden combinar mediante los patrones and , or y not . Se
pueden agregar paréntesis para aclarar patrones más complejos.
Otro conjunto de características admite la informática de alto rendimiento en C#:
Los tipos nint y nuint modelan los tipos enteros de tamaño nativo en la CPU de destino.
Los punteros de función proporcionan una funcionalidad similar a la de un delegado, al mismo tiempo que
evitan las asignaciones necesarias para crear un objeto delegado.
La instrucción localsinit se puede omitir para guardar instrucciones.
Otro conjunto de mejoras admite escenarios en los que los generadores de código agregan funcionalidad:
Los inicializadores de módulo son métodos a los que el runtime llama cuando se carga un ensamblado.
Los métodos parciales admiten nuevos modificadores de acceso y tipos de valor devuelto distintos de void.
En esos casos, se debe proporcionar una implementación.
C# 9.0 agrega muchas otras pequeñas características que mejoran la productividad del desarrollador, tanto para
escribir como para leer código:
Expresiones new con tipo de destino
Funciones anónimas static
Expresiones condicionales con tipo de destino
Compatibilidad con extensiones GetEnumerator() para bucles foreach
Las expresiones lambda pueden declarar parámetros de descarte
Los atributos se pueden aplicar a las funciones locales
La versión C# 9.0 continúa el trabajo para hacer que C# siga siendo un lenguaje de programación moderno y de
uso general. Las características siguen siendo compatibles con cargas de trabajo y tipos de aplicación modernos.
Artículo publicado originalmente en el blog de NDepend , por cortesía de Erik Dietrich y Patrick Smacchia.
Relaciones entre características de lenguaje y tipos
de biblioteca
16/09/2021 • 2 minutes to read
La definición del lenguaje C# exige que una biblioteca estándar tenga determinados tipos y determinados
miembros accesibles en esos tipos. El compilador genera código que usa estos miembros y tipos necesarios
para muchas características de lenguaje diferentes. En caso necesario, hay paquetes NuGet que contienen tipos
necesarios para las versiones más recientes del lenguaje al escribir código para entornos donde esos tipos o
miembros aún no se han implementado.
Esta dependencia de la funcionalidad de la biblioteca estándar ha formado parte del lenguaje C# desde su
primera versión. En esa versión, los ejemplos incluían:
Exception: usado para todas las excepciones generadas por el compilador.
String: el tipo string de C# es sinónimo de String.
Int32: sinónimo de int .
Esa primera versión era simple: el compilador y la biblioteca estándar se distribuían juntos y solo había una
versión de cada uno.
Las versiones posteriores de C# a veces han agregado nuevos tipos o miembros a las dependencias. Los
ejemplos incluyen: INotifyCompletion, CallerFilePathAttribute y CallerMemberNameAttribute. C# 7.0 continúa
esta tendencia al agregar una dependencia a ValueTuple para implementar la característica de lenguaje tuplas.
El equipo de diseño del lenguaje se esfuerza por minimizar el área expuesta de los tipos y miembros necesarios
en una biblioteca estándar compatible. Ese objetivo está equilibrado con un diseño limpio donde las nuevas
características de la biblioteca se han incorporado sin problemas al lenguaje. Habrá nuevas características en
versiones futuras de C# que exijan nuevos tipos y miembros en una biblioteca estándar. Es importante entender
cómo administrar esas dependencias en el trabajo.
Administración de dependencias
Las herramientas del compilador de C# ahora se han desvinculado del ciclo de versiones de las bibliotecas de
.NET en las plataformas compatibles. De hecho, las distintas bibliotecas de .NET tienen ciclos de versiones
diferentes: .NET Framework en Windows se distribuye como una actualización de Windows, .NET Core se
distribuye conforme a una programación independiente y las versiones de Xamarin de actualizaciones de la
biblioteca se incluyen en las herramientas de Xamarin de cada plataforma de destino.
En la mayoría de las ocasiones estos cambios no son perceptibles. Pero cuando trabaje con una versión más
reciente del lenguaje que requiera características aún no incluidas en las bibliotecas de .NET de esa plataforma,
haga referencia a los paquetes NuGet para proporcionar esos nuevos tipos. A medida que las plataformas que
admite la aplicación se actualicen con las nuevas instalaciones del marco de trabajo, puede quitar la referencia
adicional.
Esta desvinculación significa que puede usar nuevas características de lenguaje aun cuando el destino sean
equipos que no tengan el marco de trabajo correspondiente.
Consideraciones sobre versiones y actualizaciones
para desarrolladores de C#
16/09/2021 • 2 minutes to read
La compatibilidad es un objetivo muy importante cuando se agregan nuevas características al lenguaje C#. En la
mayoría de los casos, se puede volver a compilar el código existente con una nueva versión de compilador sin
ningún problema.
Al adoptar nuevas características del lenguaje en una biblioteca, puede requerirse más atención. Puede crear
una biblioteca con características que se encuentran en la última versión y necesita asegurarse de que las
aplicaciones creadas con versiones anteriores del compilador pueden usarla. O bien puede actualizar una
biblioteca existente, con la posibilidad de que muchos de los usuarios aún no tengan versiones actualizadas.
Cuando tome decisiones sobre la adopción de nuevas características, deberá tener en cuenta dos variaciones de
compatibilidad: la compatibilidad binaria y la compatibilidad con el origen.
Cambios incompatibles
Si un cambio no es compatible con el origen ni compatible con elementos binarios , se necesitan
cambios en el código fuente junto con la recompilación en las bibliotecas y las aplicaciones dependientes.
Evaluar la biblioteca
Estos conceptos de compatibilidad afectan a las declaraciones públicas y protegidas de la biblioteca, no a su
implementación interna. La adopción interna de nuevas características siempre es compatible con elementos
binarios .
Los cambios compatibles con elementos binarios proporcionan la nueva sintaxis que genera el mismo
código compilado para las declaraciones públicas que la sintaxis anterior. Por ejemplo, cambiar un método a un
miembro con cuerpo de expresión es un cambio compatible con elementos binarios :
Código original:
Nuevo código:
public double CalculateSquare(double value) => value * value;
Los cambios compatibles con el origen presentan sintaxis que cambia el código compilado para un miembro
público, pero de una forma compatible con los sitios de llamada existentes. Por ejemplo, cambiar una firma de
método de un parámetro por valor a un parámetro por referencia in es compatible con el origen, pero no con
elementos binarios:
Código original:
Nuevo código:
En el artículo de novedades, observe si la inclusión de una característica que afecta a las declaraciones públicas
es compatible con el origen o con elementos binarios.
Creación de tipos de registros
16/09/2021 • 12 minutes to read
C# 9 incorpora registros, un nuevo tipo de referencia que se puede crear en lugar de clases o structs. Los
registros se diferencian de las clases en que los tipos de registros usan igualdad basada en valores. Dos
variables de un tipo de registro son iguales si las definiciones del tipo de registro son idénticas y si, en cada
campo, los valores de ambos registros son iguales. Dos variables de un tipo de clase son iguales si los objetos a
los que se hace referencia son el mismo tipo de clase y las variables hacen referencia al mismo objeto. La
igualdad basada en valores conlleva otras capacidades que probablemente quiera en los tipos de registros. El
compilador genera muchos de esos miembros al declarar un elemento record en lugar de class .
En este tutorial, aprenderá a:
Decidir si debe declarar un elemento class o record .
Declarar tipos de registros y tipos de registros posicionales.
Reemplazar los métodos por métodos generados por el compilador en los registros.
Prerrequisitos
Tiene que configurar la máquina para ejecutar .NET 5 o posterior, incluido el compilador de C# 9.0 o posterior. El
compilador de C# 9.0 está disponible a partir de la versión 16.8 de Visual Studio 2019 o del SDK de .NET 5.0.
El código anterior define un registro posicional. Ha creado un tipo de referencia que contiene dos propiedades:
HighTemp y LowTemp . Esas propiedades son propiedades init-only, lo que significa que se pueden establecer en
el constructor o mediante un inicializador de propiedad. El tipo DailyTemperature también tiene un constructor
primario con dos parámetros que coinciden con las dos propiedades. Use el constructor primario para inicializar
un registro DailyTemperature :
Puede agregar sus propias propiedades o métodos a los registros, incluidos los registros posicionales. Tiene que
calcular la temperatura media de cada día. Puede agregar esa propiedad al registro DailyTemperature :
Vamos a asegurarnos de que puede usar estos datos. Agregue el código siguiente al método Main :
Ejecute la aplicación y verá un resultado similar a la siguiente pantalla (se han quitado varias filas por motivos
de espacio):
El código anterior muestra el resultado de la invalidación de ToString sintetizada por el compilador. Si prefiere
otro texto, puede escribir una versión propia de ToString que impida al compilador sintetizar otra para el
usuario.
Estas reglas son más fáciles de asimilar si se entiende el propósito de PrintMembers . PrintMembers agrega
información sobre cada propiedad de un tipo de registro a una cadena. El contrato requiere que los registros
base agreguen sus miembros a la pantalla y da por hecho que los miembros derivados van a agregar sus
miembros. Cada tipo de registro sintetiza una invalidación de ToString que es similar al ejemplo siguiente de
HeatingDegreeDays :
La firma declara un método virtual protected para que coincida con la versión del compilador. No se preocupe
si obtiene los descriptores de acceso incorrectos; el lenguaje aplica la firma correcta. Si olvida los modificadores
correctos de cualquier método sintetizado, el compilador emite advertencias o errores que ayudan a obtener la
firma correcta.
En C# 10.0 y versiones posteriores, puede declarar el método ToString como sealed en un tipo de registro.
Esto evita que los registros derivados proporcionen una implementación nueva. Los registros derivados
seguirán conteniendo la invalidación de PrintMembers . Lo haría si no quisiera que el método ToString
mostrara el tipo de entorno de ejecución del registro. En el ejemplo anterior, perdería la información sobre
dónde mide el registro los días con temperaturas altas o bajas.
Mutación no destructiva
Los miembros sintetizados de un registro posicional no modifican el estado del registro. El objetivo es que se
puedan crear registros inmutables más fácilmente. Vuelva a observar las declaraciones anteriores de
HeatingDegreeDays y CoolingDegreeDays . Los miembros agregados realizan cálculos en los valores del registro,
pero no mutan el estado. Los registros posicionales facilitan la creación de tipos de referencia inmutables.
La creación de tipos de referencia inmutables significa que se recomienda usar la mutación no destructiva. Cree
nuevas instancias de registro que sean similares a las instancias de registro existentes mediante expresiones
with . Estas expresiones son una construcción de copia con asignaciones adicionales que modifican la copia. El
resultado es una nueva instancia de registro en la que se ha copiado cada propiedad del registro existente y,
opcionalmente, se ha modificado. El registro original no se ha modificado.
Vamos a agregar un par de características al programa que muestren expresiones with . En primer lugar, vamos
a crear un nuevo registro para calcular la suma térmica con los mismos datos. La suma térmica suele usar 41F
como base de referencia y mide las temperaturas por encima de ella. Para usar los mismos datos, puede crear
un nuevo registro similar a coolingDegreeDays , pero con otra temperatura base:
Puede comparar el número de grados calculado con los números generados con una temperatura de base de
referencia superior. Recuerde que los registros son tipos de referencia y estas copias son instantáneas. No se
copia la matriz de los datos, sino que ambos registros hacen referencia a los mismos datos. Ese hecho supone
una ventaja en otro escenario. En el caso de la suma térmica, es útil realizar un seguimiento del total de los cinco
días anteriores. Puede crear nuevos registros con otros datos de origen mediante expresiones with . En el
código siguiente se compila una colección de estas acumulaciones y luego se muestran los valores:
// showing moving accumulation of 5 days using range syntax
List<CoolingDegreeDays> movingAccumulation = new();
int rangeSize = (data.Length > 5) ? 5 : data.Length;
for (int start = 0; start < data.Length - rangeSize; start++)
{
var fiveDayTotal = growingDegreeDays with { TempRecords = data[start..(start + rangeSize)] };
movingAccumulation.Add(fiveDayTotal);
}
Console.WriteLine();
Console.WriteLine("Total degree days in the last five days");
foreach(var item in movingAccumulation)
{
Console.WriteLine(item);
}
También puede usar expresiones with para crear copias de registros. No especifique ninguna propiedad entre
las llaves de la expresión with . Eso significa crear una copia y no cambiar ninguna propiedad:
Resumen
En este tutorial se han mostrado varios aspectos de los registros. Los registros proporcionan una sintaxis
concisa para los tipos de referencia cuyo uso fundamental es el almacenamiento de datos. En el caso de las
clases orientadas a objetos, el uso fundamental es definir responsabilidades. Este tutorial se ha centrado en los
registros posicionales, donde se puede usar una sintaxis concisa para declarar las propiedades init-only de un
registro. El compilador sintetiza varios miembros del registro para copiar y comparar registros. Puede agregar
cualquier otro miembro que necesite para sus tipos de registros. Puede crear tipos de registros inmutables
sabiendo que ninguno de los miembros generados por el compilador mutaría su estado. Además, las
expresiones with facilitan la compatibilidad con la mutación no destructiva.
Los registros presentan otra manera de definir tipos. Se usan definiciones class para crear jerarquías
orientadas a objetos que se centran en las responsabilidades y el comportamiento de los objetos. Cree tipos
struct para las estructuras de datos que almacenan datos y que son lo suficientemente pequeñas como para
copiarse de forma eficaz. Cree tipos record si lo que busca son comparaciones y análisis de similitud que se
basen en valores, y quiere usar variables de referencia, pero no copiar valores.
Puede obtener una descripción completa de los registros en el artículo de referencia del lenguaje C# para el tipo
de registro y la especificación de tipo de registro propuesta.
Tutorial: Exploración de ideas mediante
instrucciones de nivel superior para compilar código
mientras aprende
16/09/2021 • 8 minutes to read
Requisitos previos
Tendrá que configurar el equipo para ejecutar .NET 6, que incluye el compilador de C# 10.0. El compilador de
C# 10.0 está disponible a partir de Visual Studio 2022 o del SDK de .NET 6.0.
En este tutorial se da por supuesto que está familiarizado con C# y. NET, incluidos Visual Studio o la CLI de .NET
Core.
Comienzo de la exploración
Las instrucciones de nivel superior permiten evitar la ceremonia adicional que requiere colocar el punto de
entrada del programa en un método estático en una clase. El punto de partida típico de una aplicación de
consola nueva es similar al código siguiente:
using System;
namespace Application
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}
El código anterior es el resultado de ejecutar el comando dotnet new console y crear una aplicación de consola.
Estas 11 líneas solo contienen una línea de código ejecutable. Puede simplificar ese programa con la nueva
característica de instrucciones de nivel superior. Esto le permite quitar todas las líneas de este programa menos
dos:
Esta característica simplifica lo que se necesita para comenzar a explorar nuevas ideas. Puede usar las
instrucciones de nivel superior para escenarios de scripting o para explorar. Una vez que conozca los aspectos
básicos, puede empezar a refactorizar el código y crear métodos, clases u otros ensamblados para los
componentes reutilizables que ha compilado. Las instrucciones de nivel superior permiten una experimentación
rápida y tutoriales para principiantes. También proporcionan una ruta fluida desde la experimentación hasta la
obtención de programas completos.
Las instrucciones de nivel superior se ejecutan en el orden en el que aparecen en el archivo. Las instrucciones de
nivel superior solo se pueden usar en un archivo de código fuente de la aplicación. El compilador genera un
error si se usan en más de un archivo.
Console.WriteLine(args);
No declare una variable args . Para el único archivo de código fuente que contiene las instrucciones de nivel
superior, el compilador reconoce args para indicar los argumentos de línea de comandos. El tipo de args es
string[] , como en todos los programas de C#.
Los argumentos después de -- en la línea de comandos se pasan al programa. Puede ver el tipo de la variable
args , ya que es lo que se imprime en la consola:
System.String[]
Para escribir la pregunta en la consola, tendrá que enumerar los argumentos y separarlos con un espacio.
Reemplace la llamada a WriteLine por el código siguiente:
Console.WriteLine();
foreach(var s in args)
{
Console.Write(s);
Console.Write(' ');
}
Console.WriteLine();
Ahora, al ejecutar el programa, mostrará correctamente la pregunta como una cadena de argumentos.
string[] answers =
{
"It is certain.", "Reply hazy, try again.", "Don’t count on it.",
"It is decidedly so.", "Ask again later.", "My reply is no.",
"Without a doubt.", "Better not tell you now.", "My sources say no.",
"Yes – definitely.", "Cannot predict now.", "Outlook not so good.",
"You may rely on it.", "Concentrate and ask again.", "Very doubtful.",
"As I see it, yes.",
"Most likely.",
"Outlook good.",
"Yes.",
"Signs point to yes.",
};
Esta matriz tiene diez respuestas que son afirmativas, cinco inexpresivas y cinco negativas. A continuación,
agregue el código siguiente para generar y mostrar una respuesta aleatoria de la matriz:
Puede volver a ejecutar la aplicación para ver los resultados. Debería ver algo parecido a la salida siguiente:
Este código responde a las preguntas, pero agreguemos una característica más. Quiere que la aplicación de
preguntas simule que se piensa la respuesta. Puede hacerlo si agrega una animación de ASCII y se detiene
mientras trabaja. Agregue el código siguiente después de la línea que reproduce la pregunta:
for (int i = 0; i < 20; i++)
{
Console.Write("| -");
await Task.Delay(50);
Console.Write("\b\b\b");
Console.Write("/ \\");
await Task.Delay(50);
Console.Write("\b\b\b");
Console.Write("- |");
await Task.Delay(50);
Console.Write("\b\b\b");
Console.Write("\\ /");
await Task.Delay(50);
Console.Write("\b\b\b");
}
Console.WriteLine();
También tendrá que agregar una instrucción using a la parte superior del archivo de código fuente:
using System.Threading.Tasks;
Las instrucciones using deben aparecer antes que cualquier otra del archivo. De lo contrario, es un error del
compilador. Puede volver a ejecutar el programa y ver la animación. Eso mejora la experiencia. Experimente con
la duración del retraso hasta que le guste.
El código anterior crea un conjunto de líneas giratorias separadas por un espacio. Al agregar la palabra clave
await se le indica al compilador que genere el punto de entrada del programa como un método con el
modificador async y que devuelva System.Threading.Tasks.Task. Este programa no devuelve un valor, por lo que
el punto de entrada del programa devuelve Task . Si el programa devuelve un valor entero, tendría que agregar
una instrucción return al final de las instrucciones de nivel superior. Esa instrucción return especificaría el valor
entero que se va a devolver. Si las instrucciones de nivel superior incluyen una expresión await , el tipo de valor
devuelto se convierte en System.Threading.Tasks.Task<TResult>.
string[] answers =
{
"It is certain.", "Reply hazy, try again.", "Don't count on it.",
"It is decidedly so.", "Ask again later.", "My reply is no.",
"Without a doubt.", "Better not tell you now.", "My sources say no.",
"Yes – definitely.", "Cannot predict now.", "Outlook not so good.",
"You may rely on it.", "Concentrate and ask again.", "Very doubtful.",
"As I see it, yes.",
"Most likely.",
"Outlook good.",
"Yes.",
"Signs point to yes.",
};
En el código anterior es razonable. Funciona. Pero no es reutilizable. Ahora que ya tiene la aplicación en
funcionamiento, es momento de extraer los elementos reutilizables.
Un candidato es el código que muestra la animación en espera. Ese fragmento de código se puede convertir en
un método:
Puede empezar por crear una función local en el archivo. Reemplace la animación actual por el código siguiente:
await ShowConsoleAnimation();
En el código anterior se crea una función local dentro del método Main. Todavía no es reutilizable. Por tanto,
extraiga ese código en una clase. Cree un archivo con el nombre utilities.cs y agregue el código siguiente:
namespace MyNamespace
{
public static class Utilities
{
public static async Task ShowConsoleAnimation()
{
for (int i = 0; i < 20; i++)
{
Console.Write("| -");
await Task.Delay(50);
Console.Write("\b\b\b");
Console.Write("/ \\");
await Task.Delay(50);
Console.Write("\b\b\b");
Console.Write("- |");
await Task.Delay(50);
Console.Write("\b\b\b");
Console.Write("\\ /");
await Task.Delay(50);
Console.Write("\b\b\b");
}
Console.WriteLine();
}
}
}
Un archivo que tiene instrucciones de nivel superior también puede contener espacios de nombres y tipos al
final del archivo, después de las instrucciones de nivel superior. Pero para este tutorial, coloque el método de
animación en un archivo independiente para que sea más fácil de usar.
Por último, puede limpiar el código de animación para quitar duplicaciones:
foreach (string s in new[] { "| -", "/ \\", "- |", "\\ /", })
{
Console.Write(s);
await Task.Delay(50);
Console.Write("\b\b\b");
}
Ahora tiene una aplicación completa y ha refactorizado los elementos reutilizables para su uso posterior. Puede
llamar al nuevo método de utilidad desde las instrucciones de nivel superior, tal como se muestra a continuación
en la versión finalizada del programa principal:
using MyNamespace;
Console.WriteLine();
foreach(var s in args)
{
Console.Write(s);
Console.Write(' ');
}
Console.WriteLine();
await Utilities.ShowConsoleAnimation();
string[] answers =
{
"It is certain.", "Reply hazy, try again.", "Don’t count on it.",
"It is decidedly so.", "Ask again later.", "My reply is no.",
"Without a doubt.", "Better not tell you now.", "My sources say no.",
"Yes – definitely.", "Cannot predict now.", "Outlook not so good.",
"You may rely on it.", "Concentrate and ask again.", "Very doubtful.",
"As I see it, yes.",
"Most likely.",
"Outlook good.",
"Yes.",
"Signs point to yes.",
};
Resumen
Las instrucciones de nivel superior facilitan la creación de programas sencillos para explorar nuevos algoritmos.
Puede experimentar con algoritmos si prueba otros fragmentos de código. Una vez que haya aprendido lo que
funciona, puede refactorizar el código para que sea más fácil de mantener.
Las instrucciones de nivel superior simplifican los programas basados en aplicaciones de consola. Esto incluye
Azure Functions, las acciones de GitHub y otras utilidades pequeñas. Para obtener más información, vea
Instrucciones de nivel superior (Guía de programación de C#).
Uso de la coincidencia de patrones para crear el
comportamiento de la clase y mejorar el código
16/09/2021 • 11 minutes to read
Las características de coincidencia de patrones en C# proporcionan la sintaxis para expresar los algoritmos.
Puede usar estas técnicas para implementar el comportamiento en las clases. Puede combinar el diseño de
clases orientadas a objetos con una implementación orientada a datos para proporcionar un código conciso al
modelar objetos del mundo real.
En este tutorial, aprenderá a:
Expresar las clases orientadas a objetos mediante patrones de datos.
Implementar dichos patrones con las características de coincidencia de patrones de C#.
Aprovechar los diagnósticos del compilador para validar la implementación.
Requisitos previos
Deberá configurar la máquina para ejecutar .NET 5, incluido el compilador de C# 9.0. El compilador de C# 9.0
está disponible a partir de Visual Studio 2019, versión 16.8 Preview o del SDK de .NET 5.0 Preview.
El código anterior inicializa el objeto de manera que ambas compuertas estén cerradas y el nivel del agua sea
bajo. Luego, escriba el código de prueba siguiente en el método Main como guía para crear una primera
implementación de la clase:
// Create a new canal lock:
var canalGate = new CanalLock();
canalGate.SetLowGate(open: true);
Console.WriteLine($"Open the lower gate: {canalGate}");
canalGate.SetLowGate(open: false);
Console.WriteLine($"Close the lower gate: {canalGate}");
canalGate.SetWaterLevel(WaterLevel.High);
Console.WriteLine($"Raise the water level: {canalGate}");
Console.WriteLine(canalGate);
canalGate.SetHighGate(open: true);
Console.WriteLine($"Open the higher gate: {canalGate}");
canalGate.SetHighGate(open: false);
Console.WriteLine($"Close the higher gate: {canalGate}");
canalGate.SetWaterLevel(WaterLevel.Low);
Console.WriteLine($"Lower the water level: {canalGate}");
canalGate.SetLowGate(open: true);
Console.WriteLine($"Open the lower gate: {canalGate}");
canalGate.SetLowGate(open: false);
Console.WriteLine($"Close the lower gate: {canalGate}");
A continuación, agregue una primera implementación de cada método en la clase CanalLock . El código
siguiente implementa los métodos de la clase sin preocuparse por las reglas de seguridad. Más adelante
agregará pruebas de seguridad:
Las pruebas que ha escrito hasta el momento se completan correctamente. Ha implementado los aspectos
básicos. Ahora, escriba una prueba para la primera condición de error. Al final de las pruebas anteriores, ambas
compuertas están cerradas y el nivel del agua se establece en bajo. Agregue una prueba para intentar abrir la
compuerta superior:
Console.WriteLine("=============================================");
Console.WriteLine(" Test invalid commands");
// Open "wrong" gate (2 tests)
try
{
canalGate = new CanalLock();
canalGate.SetHighGate(open: true);
}
catch (InvalidOperationException)
{
Console.WriteLine("Invalid operation: Can't open the high gate. Water is low.");
}
Console.WriteLine($"Try to open upper gate: {canalGate}");
Esta prueba genera un error porque la compuerta se abre. Como primera implementación, puede corregirlo con
este código:
Las pruebas se realizan correctamente. Pero a medida que agrega más pruebas, agregará cada vez más
cláusulas if y probará distintas propiedades. Pronto, estos métodos resultarán demasiado complicados a
medida que agregue más condicionales.
Pruebe esta versión. Las pruebas se realizan correctamente, lo que valida el código. En la tabla completa se
muestran las combinaciones posibles de entradas y resultados. Eso significa que tanto usted como otros
desarrolladores pueden examinar rápidamente la tabla y ver que se han cubierto todas las entradas posibles.
Usar el compilador puede hacerlo más fácil. Después de agregar el código anterior, puede ver que el compilador
genera una advertencia: CS8524 indica que la expresión switch no cubre todas las entradas posibles. El motivo
de esta advertencia es que una de las entradas es de tipo enum . El compilador interpreta "todas las entradas
posibles" como todas las entradas del tipo subyacente, por lo general, un int . Esta expresión switch solo
comprueba los valores declarados en la enum . Para quitar la advertencia, puede agregar un patrón de descarte
comodín para el último segmento de la expresión. Esta condición genera una excepción porque indica una
entrada no válida:
El segmento modificador anterior debe ir al final de la expresión switch porque coincide con todas las
entradas. Experimente poniendo el segmento modificador antes en la expresión. Eso generará un error de
compilador CS8510 para un código inalcanzable en un patrón. La estructura natural de las expresiones switch
permite que el compilador genere errores y advertencias en caso de posibles errores. La "red de seguridad" del
compilador facilita la creación de código correcto en menos iteraciones y brinda la libertad de combinar
segmentos modificadores con caracteres comodín. El compilador emitirá errores si la combinación da como
resultado segmentos inaccesibles no esperados y advertencias si quita un segmento necesario.
El primer cambio consiste en combinar todos los segmentos en los que el comando va a cerrar la compuerta;
eso siempre se permite. Agregue el código siguiente como el primer segmento de la expresión switch:
Después de agregar el segmento modificador anterior, recibirá cuatro errores de compilador, uno en cada uno
de los segmentos donde el comando es false . Estos segmentos ya los cubre el segmento recién agregado.
Puede quitar sin problemas esas cuatro líneas. Su intención era que este segmento modificador nuevo
reemplazara esas condiciones.
Luego, puede simplificar los cuatro segmentos en los que el comando indica abrir la compuerta. En ambos
casos en los que el nivel del agua es alto, se puede abrir la compuerta. (En un caso, ya está abierta). Un caso en
el que el nivel del agua es bajo genera una excepción y el otro no debería ocurrir. Debería ser seguro generar la
misma excepción si el cierre hidráulico ya tiene un estado no válido. Puede hacer estas simplificaciones para
esos segmentos:
(true, _, WaterLevel.High) => true,
(true, false, WaterLevel.Low) => throw new InvalidOperationException("Cannot open high gate when the water
is low"),
_ => throw new InvalidOperationException("Invalid internal state"),
Vuelva a ejecutar las pruebas y las completarán correctamente. Esta es la versión final del método SetHighGate :
Vuelva a ejecutar la aplicación. Puede ver que se generan errores en las pruebas nuevas y que la esclusa de
canal queda en un estado no válido. Intente implementar usted mismo el resto de los métodos. El método para
establecer la compuerta inferior debe ser similar al que se usa para establecer la compuerta superior. El método
que cambia el nivel del agua tiene otras comprobaciones, pero debe seguir una estructura similar. Puede que le
resulte útil usar el mismo proceso para el método que establece el nivel del agua. Comience con las cuatro
entradas: El estado de ambas compuertas, el estado actual del nivel del agua y el nuevo nivel del agua solicitado.
La expresión switch debe empezar por:
Tendrá que completar un total de 16 segmentos modificadores. Luego, realice la prueba y simplifique.
¿Hizo métodos como este?
// Change the lower gate.
public void SetLowGate(bool open)
{
LowWaterGateOpen = (open, LowWaterGateOpen, CanalLockWaterLevel) switch
{
(false, _, _) => false,
(true, _, WaterLevel.Low) => true,
(true, false, WaterLevel.High) => throw new InvalidOperationException("Cannot open high gate when
the water is low"),
_ => throw new InvalidOperationException("Invalid internal state"),
};
}
Las pruebas deberían completarse correctamente y la esclusa de canal debería funcionar de manera segura.
Resumen
En este tutorial, aprendió a usar la coincidencia de patrones para comprobar el estado interno de un objeto
antes de aplicar cualquier cambio en ese estado. Puede comprobar combinaciones de propiedades. Una vez que
cree tablas para cualquiera de esas transiciones, probará el código y, luego, lo simplificará para su lectura y
mantenimiento. Estas refactorizaciones iniciales pueden sugerir otras refactorizaciones que validen el estado
interno o administren otros cambios de la API. En este tutorial se combinan clases y objetos con un enfoque más
orientado a datos basado en patrones para implementar esas clases.
Tutorial: Actualización de las interfaces mediante el
uso de métodos de interfaz predeterminados en C#
8.0
16/09/2021 • 6 minutes to read
A partir de C# 8.0 en .NET Core 3.0, puede definir una implementación cuando declare un miembro de una
interfaz. El escenario más común es agregar de forma segura a miembros a una interfaz ya publicada y utilizada
por clientes incontables.
En este tutorial aprenderá lo siguiente:
Extender interfaces de forma segura mediante la adición de métodos con implementaciones.
Crear implementaciones con parámetros para proporcionar mayor flexibilidad.
Permitir que los implementadores proporcionen una implementación más específica en forma de una
invalidación.
Requisitos previos
Deberá configurar la máquina para ejecutar .NET Core, incluido el compilador de C# 8.0. El compilador de C# 8.0
está disponible a partir de la versión 16.3 de Visual Studio 2019 o del SDK de .NET Core 3.0.
En esas interfaces, el equipo pudo generar una biblioteca para sus usuarios con el fin de crear una mejor
experiencia para los clientes. Su objetivo era crear una relación más estrecha con los clientes existentes y
mejorar sus relaciones con los clientes nuevos.
Ahora, es momento de actualizar la biblioteca para la próxima versión. Una de las características solicitadas
permite un descuento por fidelidad para los clientes que tienen muchos pedidos. Este nuevo descuento por
fidelidad se aplica cada vez que un cliente realiza un pedido. El descuento específico es una propiedad de cada
cliente individual. Cada implementación de ICustomer puede establecer reglas diferentes para el descuento por
fidelidad.
La forma más natural para agregar esta funcionalidad es mejorar la interfaz ICustomer con un método para
aplicar los descuentos por fidelización. Esta sugerencia de diseño es motivo de preocupación entre los
desarrolladores experimentados: "Las interfaces son inmutables una vez que se han publicado. ¡Es un cambio
importante!" C# 8.0 agrega las implementaciones de interfaz predeterminadas para actualizar las interfaces. Los
autores de bibliotecas pueden agregar a nuevos miembros a la interfaz y proporcionar una implementación
predeterminada para esos miembros.
Las implementaciones de interfaces predeterminadas permiten a los desarrolladores actualizar una interfaz
mientras siguen permitiendo que los implementadores invaliden esa implementación. Los usuarios de la
biblioteca pueden aceptar la implementación predeterminada como un cambio sin importancia. Si sus reglas de
negocios son diferentes, se puede invalidar.
// Version 1:
public decimal ComputeLoyaltyDiscount()
{
DateTime TwoYearsAgo = DateTime.Now.AddYears(-2);
if ((DateJoined < TwoYearsAgo) && (PreviousOrders.Count() > 10))
{
return 0.10m;
}
return 0;
}
Proporcionar parametrización
Ese es un buen inicio. Pero la implementación predeterminada es demasiado restrictiva. Muchos consumidores
de este sistema pueden elegir diferentes umbrales para el número de compras, una longitud diferente de la
pertenencia o un porcentaje diferente del descuento. Puede proporcionar una mejor experiencia de
actualización para más clientes proporcionando una manera de establecer esos parámetros. Vamos a agregar
un método estático que establezca esos tres parámetros controlando la implementación predeterminada:
// Version 2:
public static void SetLoyaltyThresholds(
TimeSpan ago,
int minimumOrders = 10,
decimal percentageDiscount = 0.10m)
{
length = ago;
orderCount = minimumOrders;
discountPercent = percentageDiscount;
}
private static TimeSpan length = new TimeSpan(365 * 2, 0,0,0); // two years
private static int orderCount = 10;
private static decimal discountPercent = 0.10m;
Hay muchas funcionalidades nuevas de lenguaje que se muestran en este pequeño fragmento de código. Las
interfaces ahora pueden incluir miembros estáticos, incluidos campos y métodos. También están habilitados
diferentes modificadores de acceso. Los campos adicionales son privados, el nuevo método es público. Todos los
modificadores están permitidos en los miembros de la interfaz.
Las aplicaciones que usan la fórmula general para calcular el descuento por fidelidad, pero diferentes
parámetros, no necesitan proporcionar una implementación personalizada: pueden establecer los argumentos a
través de un método estático. Por ejemplo, el siguiente código establece una "apreciación de cliente" que
recompensa a cualquier cliente con más de una pertenencia al mes:
En una implementación de una clase que implementa esta interfaz, la invalidación puede llamar al método
auxiliar estático y ampliar esa lógica para proporcionar el descuento de "cliente nuevo":
Puede ver todo el código terminado en nuestro repositorio de ejemplos en GitHub. Puede obtener la aplicación
de inicio en nuestro repositorio de ejemplo en GitHub.
Estas nuevas características significan que las interfaces se pueden actualizar de forma segura cuando hay una
implementación predeterminada razonable para esos nuevos miembros. Diseñe cuidadosamente las interfaces
para expresar ideas funcionales únicas que puedan implementarse con varias clases. Esto facilita la actualización
de esas definiciones de interfaz cuando se descubren nuevos requisitos para esa misma idea funcional.
Tutorial: Funcionalidad de combinación al crear
clases mediante interfaces con métodos de interfaz
predeterminados
16/09/2021 • 9 minutes to read
A partir de C# 8.0 en .NET Core 3.0, puede definir una implementación cuando declare un miembro de una
interfaz. Esta característica proporciona nuevas funcionalidades en las que puede definir implementaciones
predeterminadas para las características declaradas en las interfaces. Las clases pueden elegir cuándo invalidar
la funcionalidad, cuándo usar la funcionalidad predeterminada y cuándo no se debe declarar la compatibilidad
con características discretas.
En este tutorial aprenderá lo siguiente:
Creación de interfaces con implementaciones que describen características discretas.
Creación de clases que usan las implementaciones predeterminadas.
Creación de clases que invaliden algunas o todas las implementaciones predeterminadas.
Requisitos previos
Deberá configurar la máquina para ejecutar .NET Core, incluido el compilador de C# 8.0. El compilador de C# 8.0
está disponible a partir de la versión 16.3 de Visual Studio 2019 o del SDK de .NET Core 3.0 o versiones
posteriores.
Diseño de la aplicación
Considere una aplicación de automatización de dispositivos del hogar. Probablemente tenga muchos tipos
diferentes de luces e indicadores que podrían usarse en toda la casa. Cada luz debe admitir las API para
encenderla y apagarla, y para notificar el estado actual. Algunas luces e indicadores pueden admitir otras
características, como:
Encender la luz y apagarla después de un tiempo.
Hacer parpadear la luz durante un período.
Algunas de estas funcionalidades extendidas se pueden emular en los dispositivos que admiten el conjunto
mínimo. Lo que indica que se proporciona una implementación predeterminada. En el caso de los dispositivos
que tienen más funcionalidades integradas, el software del dispositivo usaría las funcionalidades nativas. Para
otras luces, podrían optar por implementar la interfaz y usar la implementación predeterminada.
Los miembros de interfaz predeterminados son una solución mejor para este escenario que los métodos de
extensión. Los autores de clases pueden controlar qué interfaces deciden implementar. Las interfaces que elijan
están disponibles como métodos. Además, dado que los métodos de interfaz predeterminados son virtuales de
forma predeterminada, el envío del método siempre elige la implementación en la clase.
Vamos a crear el código para mostrar estas diferencias.
Creación de interfaces
Empiece por crear la interfaz que define el comportamiento de todas las luces:
Un accesorio básico de luces de techo puede implementar esta interfaz, tal como se muestra en el código
siguiente:
public override string ToString() => $"The light is {(isOn ? "on" : "off")}";
}
En este tutorial, el código no dispone de dispositivos IoT, pero emula esas actividades escribiendo mensajes en
la consola. Puede explorar el código sin automatizar los dispositivos de hogar.
A continuación, vamos a definir la interfaz para una luz que se pueda apagar automáticamente después de un
tiempo de espera:
Podría agregar una implementación básica a la luz de techo, pero una solución mejor es modificar esta
definición de interfaz para proporcionar una implementación predeterminada virtual :
public interface ITimerLight : ILight
{
public async Task TurnOnFor(int duration)
{
Console.WriteLine("Using the default interface method for the ITimerLight.TurnOnFor.");
SwitchOn();
await Task.Delay(duration);
SwitchOff();
Console.WriteLine("Completed ITimerLight.TurnOnFor sequence.");
}
}
Al agregar ese cambio, la clase OverheadLight puede implementar la función de temporizador mediante la
declaración de la compatibilidad con la interfaz:
Un tipo de luz diferente puede admitir un protocolo más sofisticado. Puede proporcionar su propia
implementación de TurnOnFor , como se muestra en el código siguiente:
Funcionalidades de combinación
Las ventajas de los métodos de interfaz predeterminados resultan más claras a medida que se introducen
funcionalidades más avanzadas. El uso de interfaces permite combinar las funcionalidades. También permite que
cada autor de clase elija entre la implementación predeterminada y una implementación personalizada. Vamos a
agregar una interfaz con una implementación predeterminada para una luz parpadeante:
public interface IBlinkingLight : ILight
{
public async Task Blink(int duration, int repeatCount)
{
Console.WriteLine("Using the default interface method for IBlinkingLight.Blink.");
for (int count = 0; count < repeatCount; count++)
{
SwitchOn();
await Task.Delay(duration);
SwitchOff();
await Task.Delay(duration);
}
Console.WriteLine("Done with the default interface method for IBlinkingLight.Blink.");
}
}
La implementación predeterminada permite que cualquier luz parpadee. La luz de techo puede agregar las
funcionalidades de temporizador y de parpadeo mediante la implementación predeterminada:
public override string ToString() => $"The light is {(isOn ? "on" : "off")}";
}
Un nuevo tipo de luz, la clase LEDLight , admite la función de temporizador y la función de parpadeo
directamente. Este estilo de luz implementa las interfaces ITimerLight y IBlinkingLight , e invalida el método
Blink :
public override string ToString() => $"The light is {(isOn ? "on" : "off")}";
}
public override string ToString() => $"The light is {(isOn ? "on" : "off")}";
}
La clase HalogenLight que ha creado anteriormente no admite el parpadeo. Por lo tanto, no agregue la interfaz
IBlinkingLight a la lista de interfaces compatibles.
Estos cambios se compilan correctamente, aunque la clase ExtraFancyLight declara la compatibilidad con la
interfaz de ILight y las interfaces derivadas ITimerLight y IBlinkingLight . Solo hay una implementación
"más cercana" declarada en la interfaz ILight . Cualquier clase que declare una invalidación se convertirá en la
implementación "más cercana". Ha visto ejemplos en las clases anteriores que reemplazaron los miembros de
otras interfaces derivadas.
Evite reemplazar el mismo método en varias interfaces derivadas. Al hacerlo, se crea una llamada de método
ambiguo siempre que una clase implementa ambas interfaces derivadas. El compilador no puede elegir un solo
método mejor para que emita un error. Por ejemplo, si tanto IBlinkingLight como ITimerLight implementaron
una invalidación de PowerStatus , OverheadLight tendría que proporcionar una invalidación más específica. De
lo contrario, el compilador no puede elegir entre las implementaciones en las dos interfaces derivadas.
Normalmente, puede evitar esta situación al mantener las definiciones de interfaz pequeñas y centradas en una
característica. En este escenario, cada funcionalidad de una luz es su propia interfaz; solo las clases heredan
varias interfaces.
En este ejemplo se muestra un escenario en el que puede definir características discretas que se pueden
combinar en clases. Declare cualquier conjunto de funcionalidades admitidas declarando qué interfaces admite
una clase. El uso de métodos de interfaz predeterminados virtuales permite a las clases usar o definir una
implementación diferente para cualquiera de los métodos de interfaz o para todos. Esta funcionalidad del
lenguaje proporciona nuevas formas de modelar los sistemas reales que se están compilando. Los métodos de
interfaz predeterminados proporcionan una forma más clara de expresar clases relacionadas que pueden
combinar con diferentes características mediante implementaciones virtuales de esas funcionalidades.
Índices y rangos
16/09/2021 • 6 minutes to read
Los intervalos e índices proporcionan una sintaxis concisa para acceder a elementos únicos o intervalos en una
secuencia.
En este tutorial aprenderá lo siguiente:
Usar la sintaxis para intervalos de una secuencia.
Comprender las decisiones de diseño para iniciar y finalizar cada secuencia.
Descubrir escenarios para los tipos Index y Range.
Puede recuperar la última palabra con el índice ^1 . Agregue el código siguiente a la inicialización:
Un rango especifica el inicio y el final de un intervalo. Los rangos son excluyentes, lo que significa que el final no
se incluye en el intervalo. El rango [0..^0] representa todo el intervalo, al igual que [0..sequence.Length]
representa todo el intervalo.
El siguiente código crea un subrango con las palabras "quick", "brown" y "fox". Va de words[1] a words[3] . El
elemento words[4] no se encuentra en el intervalo. Agregue el código siguiente al mismo método. Cópielo y
péguelo en la parte inferior de la ventana interactiva.
string[] quickBrownFox = words[1..4];
foreach (var word in quickBrownFox)
Console.Write($"< {word} >");
Console.WriteLine();
El código siguiente devuelve el rango con "lazy" y "dog". Incluye words[^2] y words[^1] . El índice del final
words[^0] no se incluye. Agregue el código siguiente también:
En los ejemplos siguientes se crean rangos con final abierto para el inicio, el final o ambos:
También puede declarar rangos o índices como variables. La variable se puede usar luego dentro de los
caracteres [ y ] :
El ejemplo siguiente muestra muchos de los motivos para esas opciones. Modifique x , y y z para probar
diferentes combinaciones. Al experimentar, use valores donde x sea menor que y y y sea menor que z
para las combinaciones válidas. Agregue el código siguiente a un nuevo método. Pruebe diferentes
combinaciones:
int[] numbers = Enumerable.Range(0, 100).ToArray();
int x = 12;
int y = 25;
int z = 36;
IMPORTANT
El rendimiento del código que usa el operador de rango depende del tipo del operando de la secuencia.
La complejidad temporal del operador de rango depende del tipo de secuencia. Por ejemplo, si la secuencia es un valor
string o una matriz, el resultado es una copia de la sección especificada de la entrada, por lo que la complejidad
temporal es O(N) (donde N es la longitud del rango). Por otro lado, si se trata de System.Span<T> o System.Memory<T>,
el resultado hace referencia a la misma memoria auxiliar, lo que significa que no hay ninguna copia y que la operación es
O(1) .
Además de la complejidad temporal, esto provoca asignaciones y copias adicionales, lo que afecta al rendimiento. En el
código sensible al rendimiento, considere la posibilidad de usar Span<T> o Memory<T> como el tipo de secuencia, ya
que el operador de rango no realiza la asignación.
Un tipo es contable si tiene una propiedad denominada Length o Count con un captador accesible y un tipo
de valor devuelto de int . Un tipo contable que no admite índices ni rangos de manera explícita podría
admitirlos implícitamente. Para más información, consulte las secciones Compatibilidad implícita de índices y
Compatibilidad implícita de rangos de la nota de propuesta de características. Los rangos que usan la
compatibilidad implícita del rango devuelven el mismo tipo de secuencia que la secuencia de origen.
Por ejemplo, los tipos de .NET siguientes admiten tanto índices como rangos: String, Span<T> y
ReadOnlySpan<T>. List<T> admite índices, pero no rangos.
Array tiene un comportamiento con más matices. Así, las matrices de una sola dimensión admiten índices y
rangos, Las matrices multidimensionales no admiten indexadores o rangos. El indexador de una matriz
multidimensional tiene varios parámetros, no un parámetro único. Las matrices escalonadas, también
denominadas matriz de matrices, admiten tanto intervalos como indexadores. En el siguiente ejemplo se
muestra cómo iterar por una subsección rectangular de una matriz escalonada. Se itera por la sección del
centro, excluyendo la primera y las últimas tres filas, así como la primera y las dos últimas columnas de cada fila
seleccionada:
En todos los casos, el operador de rango para Array asigna una matriz para almacenar los elementos devueltos.
(int min, int max, double average) MovingAverage(int[] subSequence, Range range) =>
(
subSequence[range].Min(),
subSequence[range].Max(),
subSequence[range].Average()
);
C# 8.0 presenta tipos de referencia que admiten un valor NULL, que complementan a los tipos de referencia del
mismo modo que los tipos de valor que admiten valores NULL complementan a los tipos de valor. Declarará
una variable para que sea un tipo de referencia que acepta valores NULL anexando un elemento ? al
tipo. Por ejemplo, string? representa un elemento string que acepta valores NULL. Puede utilizar estos
nuevos tipos para expresar más claramente la intención del diseño: algunas variables siempre deben tener un
valor, y a otras les puede faltar un valor.
En este tutorial aprenderá lo siguiente:
Incorporar los tipos de referencia que aceptan valores NULL y que no aceptan valores NULL en los diseños
Habilitar las comprobaciones de tipos de referencia que aceptan valores NULL en todo el código
Escribir código en la parte en la que el compilador aplica esas decisiones de diseño
Usar la característica de referencia que acepta valores NULL en sus propios diseños
Requisitos previos
Deberá configurar la máquina para ejecutar .NET Core, incluido el compilador de C# 8.0. El compilador de C# 8.0
está disponible con Visual Studio 2019 o .NET Core 3.0.
En este tutorial se da por supuesto que está familiarizado con C# y. NET, incluidos Visual Studio o la CLI de .NET
Core.
<Nullable>enable</Nullable>
El compilador interpreta cada declaración de variable de tipo de referencia como un tipo de referencia que no
admite un valor NULL para el código en un contexto de anotaciones que admite un valor NULL habilitado.
Puede ver la primera advertencia agregando propiedades para el texto de la pregunta y el tipo de pregunta, tal y
como se muestra en el código siguiente:
namespace NullableIntroduction
{
public enum QuestionType
{
YesNo,
Number,
Text
}
Dado que no ha inicializado QuestionText , el compilador emite una advertencia de que aún no se ha inicializado
una propiedad que no acepta valores NULL. Su diseño requiere que el texto de la pregunta no acepte valores
NULL, por lo que debe agregar un constructor para inicializar ese elemento y el valor QuestionType también. La
definición de clase finalizada es similar al código siguiente:
namespace NullableIntroduction
{
public enum QuestionType
{
YesNo,
Number,
Text
}
Al agregar el constructor, se quita la advertencia. El argumento del constructor también es un tipo de referencia
que no acepta valores NULL, por lo que el compilador no emite advertencias.
A continuación, cree una clase public denominada " SurveyRun ". Esta clase contiene una lista de objetos
SurveyQuestion y métodos para agregar preguntas a la encuesta, tal como se muestra en el código siguiente:
using System.Collections.Generic;
namespace NullableIntroduction
{
public class SurveyRun
{
private List<SurveyQuestion> surveyQuestions = new List<SurveyQuestion>();
Al igual que antes, debe inicializar el objeto de lista en un valor distinto a NULL o el compilador emitirá una
advertencia. No hay ninguna comprobación de valores que aceptan valores NULL en la segunda sobrecarga de
AddQuestion porque no son necesarias: ha declarado esa variable para que no acepte valores NULL. Su valor no
puede ser null .
Cambie a Program.cs en el editor y reemplace el contenido de Main con las siguientes líneas de código:
Dado que todo el proyecto está habilitado para un contexto de anotaciones que admite un valor NULL, al pasar
null a cualquier método que espera un tipo de referencia que no admite un valor NULL, recibirá una
advertencia. Pruébelo agregando la siguiente línea a Main :
surveyRun.AddQuestion(QuestionType.Text, default);
A continuación, agregue un método static para crear nuevos participantes mediante la generación de un
identificador aleatorio:
La responsabilidad principal de esta clase es generar las respuestas para que un participante de las preguntas
de la encuesta. Esta responsabilidad implica una serie de pasos:
1. Solicitar la participación en la encuesta. Si la persona no da su consentimiento, devolver una respuesta con
valores ausentes (o NULL).
2. Realizar cada pregunta y registrar la respuesta. Las respuestas también pueden tener valores ausentes (o
NULL).
Agregue el siguiente código a la clase SurveyResponse :
private Dictionary<int, string>? surveyResponses;
public bool AnswerSurvey(IEnumerable<SurveyQuestion> questions)
{
if (ConsentToSurvey())
{
surveyResponses = new Dictionary<int, string>();
int index = 0;
foreach (var question in questions)
{
var answer = GenerateAnswer(question);
if (answer != null)
{
surveyResponses.Add(index, answer);
}
index++;
}
}
return surveyResponses != null;
}
El almacenamiento de las respuestas de la encuesta es una cadena Dictionary<int, string>? , que indica que
puede aceptar valores NULL. Está usando la nueva característica de lenguaje para declarar la intención de diseño
tanto para el compilador como para cualquiera que lea el código más adelante. Si alguna vez desreferencia
surveyResponses sin comprobar el valor null en primer lugar, obtendrá una advertencia del compilador. No
recibirá una advertencia en el método AnswerSurvey porque el compilador puede determinar que la variable
surveyResponses se estableció en un valor distinto de NULL.
Al usar null en las respuestas que faltan se resalta un punto clave para trabajar con tipos de referencia que
aceptan valores NULL: el objetivo no es quitar todos los valores null del programa. En cambio, de lo que se
trata es de garantizar que el código que escribe expresa la intención del diseño. Los valores que faltan son un
concepto necesario para expresar en el código. El valor null es una forma clara de expresar los valores que
faltan. El intento de quitar todos los valores null solo lleva a definir alguna otra forma de expresar esos valores
que faltan sin null .
A continuación, deberá escribir el método PerformSurvey en la clase SurveyRun . Agregue el código siguiente a
la clase SurveyRun :
De nuevo, la elección de que un elemento List<SurveyResponse>? acepte valores NULL indica que la respuesta
puede tener valores NULL. Esto indica que la encuesta no se ha asignado a los encuestados todavía. Tenga en
cuenta que los encuestados se agregan hasta que hayan dado su consentimiento.
El último paso para ejecutar la encuesta es agregar una llamada al realizar la encuesta al final del método Main :
surveyRun.PerformSurvey(50);
Dado que surveyResponses es un tipo de referencia que admite un valor NULL, las comprobaciones de valores
NULL son necesarias antes de desreferenciarlo. El método Answer devuelve una cadena que no admite un valor
NULL, por lo que tenemos que cubrir el caso de que falte una respuesta mediante el operador de fusión de
NULL.
A continuación, agregue estos tres miembros con forma de expresión a la clase SurveyRun :
El miembro AllParticipants debe tener en cuenta que la variable respondents podría ser aceptar valores
NULL, pero el valor devuelto no puede ser NULL. Si cambia esa expresión quitando ?? y la secuencia vacía de a
continuación, el compilador advertirá de que el método podría devolver null y su firma de devolución
devuelve un tipo que no acepta valores NULL.
Finalmente, agregue el siguiente bucle a la parte inferior del método Main :
foreach (var participant in surveyRun.AllParticipants)
{
Console.WriteLine($"Participant: {participant.Id}:");
if (participant.AnsweredSurvey)
{
for (int i = 0; i < surveyRun.Questions.Count; i++)
{
var answer = participant.Answer(i);
Console.WriteLine($"\t{surveyRun.GetQuestion(i).QuestionText} : {answer}");
}
}
else
{
Console.WriteLine("\tNo responses");
}
}
No necesita ninguna comprobación null en este código porque ha diseñado las interfaces subyacentes para
que devuelvan todos los tipos de referencia que no aceptan valores NULL.
Pasos siguientes
Más información sobre cómo migrar una aplicación existente para usar tipos de referencia que aceptan valores
NULL:
Actualización de aplicaciones para usar tipos de referencia que aceptan valores NULL
Obtenga información sobre cómo usar tipos de referencia que aceptan valores NULL al utilizar Entity
Framework:
Aspectos básicos de Entity Framework Core: trabajo con tipos de referencia que aceptan valores NULL
Tutorial: Generación y uso de secuencias
asincrónicas con C# 8.0 y .NET Core 3.0
16/09/2021 • 10 minutes to read
C# 8.0 presenta secuencias asincrónicas , que modelan un origen de datos de streaming. Las secuencias de
datos suelen recuperar o generar elementos de forma asincrónica. Las secuencias asincrónicas se basan en
nuevas interfaces introducidas en .NET Standard 2.1. Dichas interfaces se admiten en .NET Core 3.0 y versiones
posteriores. Proporcionan un modelo de programación natural para los orígenes de datos de streaming
asincrónicos.
En este tutorial aprenderá lo siguiente:
Crear un origen de datos que genera una secuencia de elementos de datos de forma asincrónica.
Utilizar ese origen de datos de forma asincrónica.
Admitir la cancelación y los contextos capturados para secuencias asincrónicas.
Reconocer cuándo la interfaz y el origen de datos nuevos son preferibles a las secuencias de datos
sincrónicas anteriores.
Requisitos previos
Deberá configurar la máquina para ejecutar .NET Core, incluido el compilador de C# 8.0. El compilador de C# 8
está disponible a partir de la versión 16.3 de Visual Studio 2019 o del SDK de .NET Core 3.0.
Deberá crear un token de acceso de GitHub para poder tener acceso al punto de conexión de GraphQL de
GitHub. Seleccione los siguientes permisos para el token de acceso de GitHub:
repo:status
public_repo
Guarde el token de acceso en un lugar seguro para usarlo a fin de obtener acceso al punto de conexión de API
de GitHub.
WARNING
Mantenga seguro su token de acceso personal. Cualquier software con su token de acceso personal podría realizar
llamadas de API de GitHub con sus derechos de acceso.
En este tutorial se da por supuesto que está familiarizado con C# y. NET, incluidos Visual Studio o la CLI de .NET
Core.
try
{
var results = await runPagedQueryAsync(client, PagedIssueQuery, "docs",
cancellationSource.Token, progressReporter);
foreach(var issue in results)
Console.WriteLine(issue);
}
catch (OperationCanceledException)
{
Console.WriteLine("Work has been cancelled");
}
}
Puede establecer una variable de entorno GitHubKey para el token de acceso personal, o bien puede reemplazar
el último argumento en la llamada a GetEnvVariable por el token de acceso personal. No coloque el código de
acceso en el código fuente si va a compartir el origen con otros usuarios. No cargue nunca códigos de acceso en
un repositorio de código fuente compartido.
Después de crear el cliente de GitHub, el código de Main crea un objeto de informe de progreso y un token de
cancelación. Una vez que se crean esos objetos, Main llama a runPagedQueryAsync para recuperar las 250
incidencias creadas más recientemente. Una vez finalizada esa tarea, se muestran los resultados.
Al ejecutar la aplicación inicial, puede realizar algunas observaciones importantes acerca de cómo se ejecuta
esta aplicación. Verá el progreso notificado para cada página devuelta desde GitHub. Puede observar una pausa
marcada antes de que GitHub devuelva cada nueva página de incidencias. Por último, se muestran las
incidencias solo después de que se hayan recuperado 10 páginas de GitHub.
Examen de la implementación
La implementación revela por qué observó el comportamiento descrito en la sección anterior. Examine el código
de runPagedQueryAsync :
private static async Task<JArray> runPagedQueryAsync(GitHubClient client, string queryText, string repoName,
CancellationToken cancel, IProgress<int> progress)
{
var issueAndPRQuery = new GraphQLRequest
{
Query = queryText
};
issueAndPRQuery.Variables["repo_name"] = repoName;
Vamos a concentrarnos en el algoritmo de paginación y la estructura asincrónica del código anterior. (Puede
consultar la documentación de GraphQL de GitHub para obtener más información sobre la API de GraphQL de
GitHub). El método runPagedQueryAsync enumera las incidencias desde la más reciente hasta la más antigua.
Solicita 25 incidencias por página y examina la estructura pageInfo de la respuesta para continuar con la
página anterior. Eso sigue al soporte de paginación estándar de GraphQL para respuestas de varias páginas. La
respuesta incluye un objeto pageInfo que incluye a su vez un valor hasPreviousPages y un valor startCursor
usado para solicitar la página anterior. Las incidencias se encuentran en la matriz nodes . El método
runPagedQueryAsync anexa estos nodos a una matriz que contiene todos los resultados de todas las páginas.
Después de recuperar y restaurar una página de resultados, runPagedQueryAsync informa del progreso y
comprueba la cancelación. Si se ha solicitado la cancelación, runPagedQueryAsync lanza un
OperationCanceledException.
Hay varios elementos en este código que se pueden mejorar. Lo más importante, runPagedQueryAsync debe
asignar el almacenamiento para todas las incidencias devueltas. Este ejemplo se detiene en 250 incidencias
porque la recuperación de todas las incidencias abiertas requeriría mucha más memoria para almacenar todas
las incidencias recuperadas. Los protocolos para admitir los informes de progreso y la cancelación hacen que el
algoritmo sea más difícil de comprender en su primera lectura. Hay más tipos y API implicados. Debe realizar un
seguimiento de las comunicaciones a través de CancellationTokenSource y su CancellationToken asociado para
comprender dónde se solicita la cancelación y dónde se concede.
El código de inicio procesa cada página a medida que se recupera, tal como se muestra en el código siguiente:
finalResults.Merge(issues(results)["nodes"]);
progress?.Report(issuesReturned);
cancel.ThrowIfCancellationRequested();
También puede quitar la declaración de finalResults anteriormente en este método y la instrucción return
que sigue al bucle modificado.
Ha terminado los cambios para generar una secuencia asincrónica. El método finalizado debería ser similar al
código siguiente:
private static async IAsyncEnumerable<JToken> runPagedQueryAsync(GitHubClient client,
string queryText, string repoName)
{
var issueAndPRQuery = new GraphQLRequest
{
Query = queryText
};
issueAndPRQuery.Variables["repo_name"] = repoName;
A continuación, cambie el código que utiliza la colección para usar la secuencia asincrónica. Busque el código
siguiente en Main que procesa la colección de incidencias:
try
{
var results = await runPagedQueryAsync(client, PagedIssueQuery, "docs",
cancellationSource.Token, progressReporter);
foreach(var issue in results)
Console.WriteLine(issue);
}
catch (OperationCanceledException)
{
Console.WriteLine("Work has been cancelled");
}
La nueva interfaz IAsyncEnumerator<T> deriva de IAsyncDisposable. Esto significa que el bucle anterior
desechará la secuencia de forma asincrónica cuando finalice el bucle. Como imaginará, el bucle es similar al
código siguiente:
int num = 0;
var enumerator = runPagedQueryAsync(client, PagedIssueQuery, "docs").GetEnumeratorAsync();
try
{
while (await enumerator.MoveNextAsync())
{
var issue = enumerator.Current;
Console.WriteLine(issue);
Console.WriteLine($"Received {++num} issues in total");
}
} finally
{
if (enumerator != null)
await enumerator.DisposeAsync();
}
Puede obtener el código para el tutorial finalizado en el repositorio dotnet/docs de la carpeta csharp/whats-
new/tutorials.
En este tutorial se muestra cómo usar la interpolación de cadenas de C# para insertar valores en una cadena de
resultado única. Escriba código de C# y vea los resultados de la compilación y la ejecución. Este tutorial contiene
una serie de lecciones para mostrarle cómo insertar valores en una cadena y dar formato a estos valores de
maneras diferentes.
En este tutorial se supone que cuenta con una máquina que puede usar para el desarrollo. El tutorial de .NET
Hola mundo en 10 minutos cuenta con instrucciones para configurar el entorno de desarrollo local en Windows,
Linux o macOS. También puede completar la versión interactiva de este tutorial en el explorador.
Este comando crea una nueva aplicación de consola de .NET Core en el directorio actual.
Abra Program.cs en su editor favorito y reemplace la línea Console.WriteLine("Hello World!"); por el código
siguiente, teniendo en cuenta que debe reemplazar <name> con su nombre:
Pruebe este código escribiendo dotnet run en la ventana de la consola. Al ejecutar el programa, se muestra una
cadena única que incluye su nombre en el saludo. La cadena que se incluye en la llamada al método WriteLine
es una expresión de cadena interpolada. Es un tipo de plantilla que permite construir una sola cadena
(denominada cadena de resultado) a partir de una cadena que incluye código incrustado. Las cadenas
interpoladas son especialmente útiles para insertar valores en una cadena o en cadenas concatenadas (unidas
entre sí).
Este sencillo ejemplo contiene los dos elementos que debe tener cada cadena interpolada:
Un literal de cadena que empieza con el carácter $ antes del carácter de comillas de apertura. No puede
haber ningún espacio entre el símbolo $ y el carácter de comillas. (Si quiere saber qué pasa si incluye
una, inserte un espacio después del carácter $ , guarde el archivo y vuelva a ejecutar el programa
escribiendo dotnet run en la ventana de la consola. El compilador de C# muestra un mensaje de error:
"error CS1056: carácter no esperado '$'").
Una o varias expresiones de interpolación. Una expresión de interpolación se indica mediante una llave
de apertura y de cierre ( { y } ). Puede colocar cualquier expresión de C# que devuelva un valor
(incluido null ) dentro de las llaves.
Probemos algunos ejemplos más de interpolación de cadenas con otros tipos de datos.
Incluir diferentes tipos de datos
En la sección anterior, se ha usado una interpolación de cadena para insertar una cadena dentro de otra, pero el
resultado de una expresión de interpolación puede ser cualquier tipo de datos. Vamos a incluir valores de
distintos tipos de datos en una cadena interpolada.
En el ejemplo siguiente, primero se define un tipo de datos de clase Vegetable que tiene una propiedad Name y
un método ToString que reemplaza el comportamiento del método Object.ToString(). El public modificador de
acceso pone ese método a disposición de cualquier código de cliente para obtener la representación de la
cadena de una instancia de Vegetable . En el ejemplo, el método Vegetable.ToString devuelve el valor de la
propiedad Name que se inicializa en el constructor Vegetable :
Luego se crea una instancia de la clase Vegetable denominada item al usar el operador new y al proporcionar
un parámetro de nombre para el constructor Vegetable :
Por último, se incluye la variable item en una cadena interpolada que también contiene un valor DateTime, un
valor Decimal y un valor de enumeración Unit . Reemplace todo el código de C# en el editor con el código
siguiente y, después, use el comando dotnet run para ejecutarlo:
using System;
Observe que la expresión de interpolación item de la cadena interpolada se resuelve en el texto "eggplant" en
la cadena de resultado. Esto se debe a que, cuando el tipo del resultado de la expresión no es una cadena, el
resultado se resuelve en una cadena de la siguiente manera:
Si la expresión de interpolación se evalúa en null , se usa una cadena vacía ("", o String.Empty).
Si la expresión de interpolación no se evalúa en null , se suele llamar al método ToString del tipo de
resultado. Puede probar esto mediante la actualización de la implementación del método
Vegetable.ToString . Podría incluso no implementar el método ToString , puesto que cada tipo de datos
tiene alguna implementación de este método. Para probar esto, comente la definición del método
Vegetable.ToString del ejemplo (para ello, coloque delante un símbolo de comentario // ). En el
resultado, se reemplaza la cadena "eggplant" por el nombre del tipo completo ("Vegetable" en este
ejemplo), que es el comportamiento predeterminado del método Object.ToString(). El comportamiento
predeterminado del método ToString para un valor de enumeración es devolver la representación de
cadena del valor.
En el resultado de este ejemplo, la fecha es demasiado precisa (el precio de "eggplant" no varía por segundos) y
el valor del precio no indica una unidad de moneda. En la sección siguiente se aprende a corregir esos
problemas al controlar el formato de representaciones de cadena de los resultados de la expresión.
Especifique una cadena de formato al colocar dos puntos (":") después de la expresión de interpolación y la
cadena de formato. "d" es una cadena de formato de fecha y hora estándar que representa el formato de fecha
corta. "C2" es una cadena de formato numérico estándar que representa un número como un valor de moneda
con dos dígitos después del separador decimal.
Una serie de tipos de las bibliotecas de .NET admiten un conjunto predefinido de cadenas de formato. Esto
incluye todos los tipos numéricos y los tipos de fecha y hora. Para obtener una lista completa de los tipos que
admiten cadenas de formato, vea Dar formato a cadenas y tipos de biblioteca de clase .NET en el artículo Aplicar
formato a tipos de .NET.
Pruebe a modificar las cadenas de formato en el editor de texto y, cada vez que realice un cambio, vuelva a
ejecutar el programa para ver cómo los cambios afectan al formato de fecha y hora y al valor numérico. Cambie
"d" en {date:d} a "t" (para mostrar el formato de hora corta), "y" (para mostrar el año y el mes) y "yyyy" (para
mostrar el año como un número de cuatro dígitos). Cambie "C2" en {price:C2} a "e" (para la notación
exponencial) y "F3" (para un valor numérico con tres dígitos después del separador decimal).
Además de controlar el formato, también puede controlar el ancho de campo y la alineación de las cadenas con
formato incluidas en la cadena de resultado. En la siguiente sección aprenderá a hacerlo.
Los nombres de los autores están alineados a la izquierda y los títulos que escribieron están alineados a la
derecha. Para especificar la alineación, se agrega una coma (",") después de una expresión de interpolación y se
designa el ancho de campo mínimo. Si el valor especificado es un número positivo, el campo se alinea a la
derecha. Si es un número negativo, el campo se alinea a la izquierda.
Pruebe a quitar el signo negativo del código {"Author",-25} y {title.Key,-25} , y vuelva a ejecutar el ejemplo,
como hace este código:
Console.WriteLine($"|{"Author",25}|{"Title",30}|");
foreach (var title in titles)
Console.WriteLine($"|{title.Key,25}|{title.Value,30}|");
En este tutorial se explica cómo usar la interpolación de cadenas para dar formato a resultados de expresión e
incluirlos en una cadena de resultado. En los ejemplos se da por hecho que ya está familiarizado con los
conceptos básicos de C# y el formato de tipos .NET. Si no conoce la interpolación de cadenas o el formato de
tipos .NET, vea antes el tutorial de interpolación de cadenas interactivo. Para más información sobre cómo
aplicar formato a tipos .NET, vea el tema Aplicar formato a tipos en .NET.
NOTE
Los ejemplos de C# de este artículo se ejecutan en el ejecutor de código en línea y área de juegos de Try.NET. Haga clic en
el botón Ejecutar para ejecutar un ejemplo en una ventana interactiva. Una vez que se ejecuta el código, puede modificar
y ejecutar el código modificado si vuelve a hacer clic en Ejecutar . El código modificado se ejecuta en la ventana interactiva
o, si se produce un error en la compilación, en la ventana interactiva se muestran todos los mensajes de error del
compilador de C#.
Introducción
La característica de interpolación de cadenas se basa en la característica de formato compuesto y proporciona
una sintaxis más legible y cómoda para incluir resultados de expresiones con formato en una cadena de
resultado.
Para distinguir un literal de cadena como una cadena interpolada, antepóngale el símbolo $ . Puede insertar
cualquier expresión de C# válida que devuelva un valor en una cadena interpolada. En el siguiente ejemplo, en
cuanto la expresión se evalúa, su resultado se convierte en una cadena y se incluye en una cadena de resultado:
double a = 3;
double b = 4;
Console.WriteLine($"Area of the right triangle with legs of {a} and {b} is {0.5 * a * b}");
Console.WriteLine($"Length of the hypotenuse of the right triangle with legs of {a} and {b} is
{CalculateHypotenuse(a, b)}");
double CalculateHypotenuse(double leg1, double leg2) => Math.Sqrt(leg1 * leg1 + leg2 * leg2);
// Expected output:
// Area of the right triangle with legs of 3 and 4 is 6
// Length of the hypotenuse of the right triangle with legs of 3 and 4 is 5
Como se ilustra en el ejemplo, para incluir una expresión en una cadena interpolada hay que meterla entre
llaves:
{<interpolationExpression>}
Las cadenas interpoladas admiten todas las funcionalidades de la característica de formato compuesto de
cadena. Esto las convierte en una alternativa más legible al uso del método String.Format.
{<interpolationExpression>:<formatString>}
En el siguiente ejemplo se muestra cómo especificar cadenas de formato estándar y personalizadas para
expresiones que generan resultados numéricos o de fecha y hora:
// Expected output:
// On Sunday, November 25, 1731 Leonhard Euler introduced the letter e to denote 2.71828 in a letter to
Christian Goldbach.
Para más información, vea la sección Format String (Componente) del tema Formatos compuestos. En esa
sección encontrará vínculos a temas en los que se describen las cadenas de formato estándar y personalizadas
compatibles con los tipos base .NET.
{<interpolationExpression>,<alignment>}
{<interpolationExpression>,<alignment>:<formatString>}
En el siguiente ejemplo se muestra cómo especificar la alineación y se emplean caracteres de barra vertical ("|")
para delimitar los campos de texto:
double a = 3;
double b = 4;
Console.WriteLine($"Three classical Pythagorean means of {a} and {b}:");
Console.WriteLine($"|{"Arithmetic",NameAlignment}|{0.5 * (a + b),ValueAlignment:F3}|");
Console.WriteLine($"|{"Geometric",NameAlignment}|{Math.Sqrt(a * b),ValueAlignment:F3}|");
Console.WriteLine($"|{"Harmonic",NameAlignment}|{2 / (1 / a + 1 / b),ValueAlignment:F3}|");
// Expected output:
// Three classical Pythagorean means of 3 and 4:
// |Arithmetic| 3.500|
// |Geometric| 3.464|
// |Harmonic | 3.429|
Tal y como refleja la salida del ejemplo, si la longitud del resultado de expresión con formato supera el ancho de
campo especificado, se omitirá el valor de alignment.
Para más información, vea la sección Alignment (Componente) del tema Formatos compuestos.
Para incluir una llave ("{" o "}") en una cadena de resultado, use dos llaves ("{{" o "}}"). Para más información, vea
la sección Llaves de escape del tema Formatos compuestos.
En el siguiente ejemplo se muestra cómo incluir llaves en una cadena de resultado y cómo construir una cadena
interpolada textual:
// Expected output:
// Find the intersection of the {1, 2, 7, 9} and {7, 9, 12} sets.
// C:\Users\Jane\Documents
// C:\Users\Jane\Documents
Tal y como se muestra en el ejemplo, se puede usar una instancia de FormattableString para generar varias
cadenas de resultado para varias referencias culturales.
Conclusión
En este tutorial se han descrito escenarios habituales en los que se usa la interpolación de cadenas. Para más
información sobre la interpolación de cadenas, vea el tema Interpolación de cadenas. Para más información
sobre cómo aplicar formato a tipos .NET, vea los temas Aplicar formato a tipos en .NET y Formatos compuestos.
Vea también
String.Format
System.FormattableString
System.IFormattable
Cadenas
Aplicación de consola
16/09/2021 • 12 minutes to read
Este tutorial le enseña varias características de .NET Core y el lenguaje C#. Aprenderá lo siguiente:
Los aspectos básicos de la CLI de .NET Core
La estructura de una aplicación de consola en C#
E/S de consola
Aspectos básicos de las API de E/S de archivo en .NET
Aspectos básicos de la programación asincrónica basada en tareas en .NET
Creará una aplicación que lea un archivo de texto y refleje el contenido de ese archivo de texto en la consola. El
ritmo de la salida a la consola se ajusta para que coincida con la lectura en voz alta. Para aumentar o reducir el
ritmo, presione las teclas "<" (menor que) o ">" (mayor que).
Hay muchas características en este tutorial. Vamos a compilarlas una a una.
Requisitos previos
Configure la máquina para ejecutar .NET Core. Puede encontrar las instrucciones de instalación en la
página Descargas de .NET Core. Puede ejecutar esta aplicación en Windows, Linux, macOS o en un
contenedor de Docker.
Instale su editor de código favorito.
Creación de la aplicación
El primer paso es crear una nueva aplicación. Abra un símbolo del sistema y cree un nuevo directorio para la
aplicación. Conviértalo en el directorio actual. Escriba el comando dotnet new console en el símbolo del sistema.
Esta acción crea los archivos de inicio para una aplicación básica "Hola mundo".
Antes de comenzar a realizar modificaciones, vamos a recorrer los pasos para ejecutar la aplicación Hola mundo
sencilla. Después de crear la aplicación, escriba dotnet restore en el símbolo del sistema. Este comando ejecuta
el proceso de restauración de paquetes de NuGet. NuGet es un administrador de paquetes .NET. Este comando
permite descargar cualquiera de las dependencias que faltan para el proyecto. Como se trata de un nuevo
proyecto, ninguna de las dependencias está en su lugar, así que con la primera ejecución se descargará .NET
Core Framework. Después de este paso inicial, solo deberá ejecutar dotnet restore al agregar nuevos paquetes
dependientes, o actualizar las versiones de cualquiera de sus dependencias.
No es necesario ejecutar dotnet restore porque lo ejecutan implícitamente todos los comandos que necesitan
que se produzca una restauración, como dotnet new , dotnet build , dotnet run , dotnet test , dotnet publish
y dotnet pack . Para deshabilitar la restauración implícita, use la opción --no-restore .
El comando dotnet restore sigue siendo válido en algunos escenarios donde tiene sentido realizar una
restauración explícita, como las compilaciones de integración continua en Azure DevOps Services o en los
sistemas de compilación que necesitan controlar explícitamente cuándo se produce la restauración.
Para obtener información sobre cómo administrar fuentes de NuGet, vea la documentación de dotnet restore .
Después de restaurar los paquetes, ejecutará dotnet build . Esta acción ejecuta el motor de compilación y crea
el ejecutable de aplicación. Por último, ejecute dotnet run para ejecutar la aplicación.
El código de la aplicación sencilla Hola a todos está todo en Program.cs. Abra ese archivo con el editor de texto
de su elección. Nos disponemos a realizar nuestros primeros cambios. En la parte superior del archivo, verá una
instrucción using:
using System;
Esta instrucción indica al compilador que cualquier tipo del espacio de nombres System está en el ámbito. Al
igual que otros lenguajes orientados a objetos que pueda haber usado, C# utiliza espacios de nombres para
organizar los tipos. Este programa Hola mundo no es diferente. Puede ver que el programa se incluye en el
espacio de nombres y el nombre se basa en el del directorio actual. Para este tutorial, vamos a cambiar el
nombre del espacio de nombres por TeleprompterConsole :
namespace TeleprompterConsole
Este método usa tipos de dos nuevos espacios de nombres. Para compilar esto, deberá agregar las dos líneas
siguientes a la parte superior del archivo:
using System.Collections.Generic;
using System.IO;
Ejecute el programa (mediante dotnet run ) y podrá ver cada línea impresa en la consola.
A continuación, debe modificar el modo en que se consumen las líneas del archivo y agregar un retraso después
de escribir cada palabra. Reemplace la instrucción Console.WriteLine(line) del método Main por el bloqueo
siguiente:
Console.Write(line);
if (!string.IsNullOrWhiteSpace(line))
{
var pause = Task.Delay(200);
// Synchronously waiting on a task is an
// anti-pattern. This will get fixed in later
// steps.
pause.Wait();
}
La clase Task está en el espacio de nombres System.Threading.Tasks, así que debe agregar esa instrucción
using en la parte superior del archivo:
using System.Threading.Tasks;
Ejecute el ejemplo y compruebe la salida. Ahora, se imprime cada palabra suelta, seguido de un retraso de 200
ms. Sin embargo, la salida mostrada indica algunos problemas porque el archivo de texto de origen tiene varias
líneas con más de 80 caracteres sin un salto de línea. Este texto puede ser difícil de leer al desplazarse por él.
Esto es fácil de corregir. Simplemente realizará el seguimiento de la longitud de cada línea y generará una nueva
línea cada vez que la longitud de la línea alcance un determinado umbral. Declare una variable local después de
la declaración de words en el método ReadFrom que contiene la longitud de línea:
var lineLength = 0;
A continuación, agregue el código siguiente después de la instrucción yield return word + " "; (antes de la
llave de cierre):
lineLength += word.Length + 1;
if (lineLength > 70)
{
yield return Environment.NewLine;
lineLength = 0;
}
Tareas asincrónicas
En este paso final, agregará el código para escribir la salida de manera asincrónica en una tarea, mientras se
ejecuta también otra tarea para leer la entrada del usuario si quiere aumentar o reducir la velocidad de la
pantalla de texto, o detendrá la presentación del texto por completo. Incluye unos cuantos pasos y, al final,
tendrá todas las actualizaciones que necesita. El primer paso es crear un método de devolución Task asincrónico
que represente el código que ha creado hasta el momento para leer y visualizar el archivo.
Agregue este método a su clase Program (se toma del cuerpo del método Main ):
Advertirá dos cambios. Primero, en el cuerpo del método, en lugar de llamar a Wait() para esperar a que finalice
una tarea de manera sincrónica, esta versión usa la palabra clave await . Para ello, debe agregar el modificador
async a la signatura del método. Este método devuelve un objeto Task . Observe que no hay ninguna
instrucción Return que devuelva un objeto Task . En su lugar, ese objeto Task se crea mediante el código que
genera el compilador cuando usa el operador await . Puede imaginar que este método devuelve cuando
alcanza un valor de await . El valor devuelto de Task indica que el trabajo no ha finalizado. El método se
reanuda cuando se completa la tarea en espera. Cuando se ha ejecutado hasta su finalización, el valor de Task
devuelto indica que se ha completado. El código de llamada puede supervisar ese valor de Task devuelto para
determinar cuándo se ha completado.
Puede llamar a este nuevo método en su método Main :
ShowTeleprompter().Wait();
Aquí, en Main , el código espera de manera sincrónica. Siempre que sea posible, debe usar el operador await
en lugar de esperar sincrónicamente. Sin embargo, en el método Main de una aplicación de consola, no puede
usar el operador await . Si así fuera, la aplicación se cerraría antes de que todas las tareas se hubieran
completado.
NOTE
Si usa C# 7.1 o una versión posterior, puede crear aplicaciones de consola con el método async Main .
A continuación, debe escribir el segundo método asincrónico para leer de la consola y controlar las teclas "<"
(menor que), ">" (mayor que), "X" o "x". Este es el método que agrega para esa tarea:
Se crea una expresión lambda que representa un delegado de Action que lee una clave de la consola y modifica
una variable local que representa el retraso cuando el usuario presiona las teclas "<" (menor que) o ">" (mayor
que). El método de delegado finaliza cuando el usuario presiona las teclas "X" o "x", que permiten al usuario
detener la presentación del texto en cualquier momento. Este método usa ReadKey() para bloquear y esperar a
que el usuario presione una tecla.
Para finalizar esta característica, debe crear un nuevo método de devolución async Task que inicie estas dos
tareas ( GetInput y ShowTeleprompter ) y también administre los datos compartidos entre ellas.
Es hora de crear una clase que controle los datos compartidos entre estas dos tareas. Esta clase contiene dos
propiedades públicas: el retraso y una marca Done para indicar que el archivo se ha leído completamente:
namespace TeleprompterConsole
{
internal class TelePrompterConfig
{
public int DelayInMilliseconds { get; private set; } = 200;
Coloque esa clase en un archivo nuevo e inclúyala en el espacio de nombres TeleprompterConsole , como se ha
mostrado anteriormente. También deberá agregar una instrucción using static para que pueda hacer
referencia a los métodos Min y Max sin la clase incluida o los nombres de espacio de nombres. Una instrucción
using static importa los métodos de una clase, mientras que las instrucciones using usadas hasta el
momento han importado todas las clases de un espacio de nombres.
A continuación, debe actualizar los métodos ShowTeleprompter y GetInput para usar el nuevo objeto config .
Escriba un método final async de devolución de Task para iniciar ambas tareas y salir cuando la primera tarea
finalice:
El método nuevo aquí es la llamada a WhenAny(Task[]). Dicha llamada crea un valor de Task que finaliza en
cuanto alguna de las tareas de su lista de argumentos se completa.
A continuación, debe actualizar los métodos ShowTeleprompter y GetInput para usar el objeto config para el
retraso:
private static async Task ShowTeleprompter(TelePrompterConfig config)
{
var words = ReadFrom("sampleQuotes.txt");
foreach (var word in words)
{
Console.Write(word);
if (!string.IsNullOrWhiteSpace(word))
{
await Task.Delay(config.DelayInMilliseconds);
}
}
config.SetDone();
}
Esta nueva versión de ShowTeleprompter llama a un nuevo método de la clase TeleprompterConfig . Ahora, debe
actualizar Main para llamar a RunTeleprompter en lugar de a ShowTeleprompter :
RunTeleprompter().Wait();
Conclusión
En este tutorial se han mostrado varias características en torno al lenguaje C# y las bibliotecas .NET Core,
relacionadas con el trabajo en aplicaciones de consola. Puede partir de este conocimiento para explorar más
sobre el lenguaje y las clases aquí presentadas. Ha visto los conceptos básicos de E/S de archivo y consola, el
uso con bloqueo y sin bloqueo de la programación asincrónica basada en tareas, un paseo por el lenguaje C# y
cómo se organizan los programas en C#. También ha conocido la interfaz de la línea de comandos y la CLI de
.NET Core.
Para obtener más información sobre la E/S de archivo, consulte el tema E/S de archivos y secuencias. Para
obtener más información sobre el modelo de programación asincrónica que se ha usado en este tutorial, vea los
temas Programación asincrónica basada en tareas y Programación asincrónica.
Tutorial: Realización de solicitudes HTTP en una
aplicación de consola de .NET mediante C#
16/09/2021 • 8 minutes to read
En este tutorial se crea una aplicación que emite solicitudes HTTP a un servicio REST en GitHub. La aplicación lee
información en formato JSON y convierte la respuesta JSON en objetos de C#. La conversión de JSON en
objetos de C# se conoce como deserialización.
En el tutorial se muestra cómo hacer lo siguiente:
Enviar solicitudes HTTP
Deserializar respuestas JSON
Configurar la deserialización con atributos
Si prefiere seguir las explicaciones con el ejemplo final del tutorial, puede descargarlo. Para obtener
instrucciones de descarga, vea Ejemplos y tutoriales.
Requisitos previos
SDK de .NET 5.0 o versiones posteriores.
Un editor de código como Visual Studio Code, que es un editor multiplataforma de código abierto. Puede
ejecutar la aplicación de ejemplo en Windows, Linux o macOS, o bien en un contenedor de Docker.
Este comando crea los archivos de inicio para una aplicación básica "Hola mundo". El nombre del
proyecto es "WebAPIClient".
3. Vaya al directorio "WebAPIClient" y ejecute la aplicación.
cd WebAPIClient
dotnet run
dotnet run ejecuta automáticamente dotnet restore para restaurar las dependencias que necesita la
aplicación. También ejecuta dotnet build si es necesario.
2. Agregue una directiva using en la parte superior del archivo Program.cs para que el compilador de C#
reconozca el tipo Task:
using System.Threading.Tasks;
Si ejecuta dotnet build en este momento, la compilación se realiza correctamente, pero advierte de que
este método no contiene ningún operador await y, por tanto, se ejecuta sincrónicamente. Agregará los
operadores await más adelante, a medida que rellene el método.
3. Reemplace el método Main con el código siguiente:
Este código:
Cambia la firma de Main al agregar el modificador async y cambiar el tipo de valor devuelto a Task .
Reemplaza la instrucción Console.WriteLine por una llamada a ProcessRepositories que usa la
palabra clave await .
4. En la clase Program , cree una instancia estática de HttpClient para controlar las solicitudes y las
respuestas.
namespace WebAPIClient
{
class Program
{
private static readonly HttpClient client = new HttpClient();
5. En el método ProcessRepositories , llame al punto de conexión de GitHub que devuelve una lista de
todos los repositorios de la organización de .NET Foundation:
private static async Task ProcessRepositories()
{
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/vnd.github.v3+json"));
client.DefaultRequestHeaders.Add("User-Agent", ".NET Foundation Repository Reporter");
Este código:
Configura encabezados HTTP para todas las solicitudes:
Un encabezado Accept para aceptar respuestas JSON.
Un encabezado User-Agent . Estos encabezados se comprueban mediante el código de servidor
de GitHub y son necesarios para recuperar información de GitHub.
Llama a HttpClient.GetStringAsync(String) para realizar una solicitud web y recuperar la respuesta.
Este método inicia una tarea que realiza la solicitud web. Cuando la solicitud se devuelve, la tarea lee
el flujo de respuesta y extrae el contenido de la secuencia. El cuerpo de la respuesta se devuelve como
un elemento String, que está disponible cuando se completa la tarea.
Espera la tarea hasta que devuelve la cadena de respuesta e imprime la respuesta en la consola.
6. Agregue dos directivas using en la parte superior del archivo:
using System.Net.Http;
using System.Net.Http.Headers;
dotnet run
using System;
namespace WebAPIClient
{
public class Repository
{
public string name { get; set; }
}
}
El código anterior define una clase para representar el objeto JSON devuelto desde la API de GitHub.
Usará esta clase para mostrar una lista de nombres de repositorio.
El objeto JSON de un objeto de repositorio contiene docenas de propiedades, pero solo se deserializará la
propiedad name . El serializador omite automáticamente las propiedades JSON para las que no hay
ninguna coincidencia en la clase de destino. Esta característica facilita la creación de tipos que funcionan
con solo un subconjunto de campos de un paquete JSON grande.
La convención de C# es poner en mayúscula la primera letra de los nombres de propiedad, pero la
propiedad name comienza aquí con minúscula porque coincide exactamente con lo que hay en JSON.
Más adelante verá cómo usar nombres de propiedad de C# que no coinciden con los nombres de
propiedad JSON.
2. Use el serializador para convertir JSON en objetos de C#. Reemplace la llamada a GetStringAsync(String)
en el método ProcessRepositories por las líneas siguientes:
por el siguiente:
using System.Collections.Generic;
using System.Text.Json;
5. Ejecutar la aplicación.
dotnet run
La salida es una lista con los nombres de los repositorios que forman parte de .NET Foundation.
Configuración de la deserialización
1. En repo.cs, cambie la propiedad name a Name y agregue un atributo [JsonPropertyName] para especificar
cómo aparece esta propiedad en JSON.
[JsonPropertyName("name")]
public string Name { get; set; }
using System.Text.Json.Serialization;
3. En Program.cs, actualice el código para aplicar el nuevo uso de las mayúsculas de la propiedad Name :
Console.WriteLine(repo.Name);
4. Ejecutar la aplicación.
La salida es la misma.
Refactorizar el código
El método ProcessRepositories puede realizar el trabajo asincrónico y devolver una colección de los
repositorios. Cambie ese método para devolver List<Repository> y mueva el código que escribe la información
al método Main .
1. Cambie la signatura de ProcessRepositories para devolver una tarea cuyo resultado sea una lista de
objetos Repository :
El compilador genera el objeto Task<T> para el valor devuelto porque ha marcado este método como
async .
3. Modifique el método Main para capturar los resultados y escribir cada nombre de repositorio en la
consola. El método Main tiene el aspecto siguiente:
[JsonPropertyName("description")]
public string Description { get; set; }
[JsonPropertyName("html_url")]
public Uri GitHubHomeUrl { get; set; }
[JsonPropertyName("homepage")]
public Uri Homepage { get; set; }
[JsonPropertyName("watchers")]
public int Watchers { get; set; }
Los tipos Uri y int tienen una funcionalidad integrada para convertir a y desde la representación de
cadena. No se necesita código adicional para deserializar desde el formato de cadena de JSON a esos
tipos de destino. Si el paquete JSON contiene datos que no se convierten en un tipo de destino, la acción
de serialización genera una excepción.
2. Actualice el método Main para mostrar los valores de propiedad:
3. Ejecutar la aplicación.
La lista ahora incluye las propiedades adicionales.
2016-02-08T21:27:00Z
Este es el formato de la hora universal coordinada (UTC), por lo que el resultado de la deserialización es un valor
DateTime cuya propiedad Kind es Utc.
Para que una fecha y hora se represente en su zona horaria, debe escribir un método de conversión
personalizado.
1. En repo.cs, agregue una propiedad public para la representación UTC de la fecha y hora y una
propiedad LastPush readonly que devuelve la fecha convertida en la hora local:
[JsonPropertyName("pushed_at")]
public DateTime LastPushUtc { get; set; }
La propiedad LastPush se define utilizando un miembro con forma de expresión para el descriptor de
acceso get . No hay ningún descriptor de acceso set . La omisión del descriptor de acceso set es una
forma de definir una propiedad de solo lectura en C#. (Sí, puede crear propiedades de solo escritura en
C#, pero su valor es limitado).
2. Agregue de nuevo otra instrucción de salida en Program.cs:
Console.WriteLine(repo.LastPush);
3. Ejecutar la aplicación.
La salida incluye la fecha y hora de la última inserción en cada repositorio.
Pasos siguientes
En este tutorial, ha creado una aplicación que realiza solicitudes web y analiza los resultados. La versión de la
aplicación debe coincidir ahora con el ejemplo terminado.
Obtenga más información sobre cómo configurar la serialización JSON en Procedimiento para serializar y
deserializar (calcular referencias y resolver referencias) JSON en .NET.
Uso de Language-Integrated Query (LINQ)
16/09/2021 • 16 minutes to read
Introducción
En este tutorial aprenderá varias características de .NET Core y el lenguaje C#. Aprenderá a:
Generar secuencias con LINQ.
Escribir métodos que puedan usarse fácilmente en las consultas LINQ.
Distinguir entre evaluación diligente y diferida.
Aprenderá estas técnicas mediante la creación de una aplicación que muestra uno de los conocimientos básicos
de cualquier mago: el orden aleatorio faro. En resumen, el orden aleatorio faro es una técnica basada en dividir
la baraja exactamente por la mitad; a continuación, el orden aleatorio intercala cada carta de cada mitad de la
baraja hasta volver a crear la original.
Los magos usan esta técnica porque cada carta está en una ubicación conocida después de cada orden aleatorio,
y el orden sigue un patrón de repetición.
Para el propósito sobre el que trata este artículo, resulta divertido ocuparnos de la manipulación de secuencias
de datos. La aplicación que se va a crear compilará una baraja de cartas y después realizará una secuencia de
órdenes aleatorios, que escribirá cada vez la secuencia completa. También podrá comparar el orden actualizado
con el original.
Este tutorial consta de varios pasos. Después de cada paso, puede ejecutar la aplicación y ver el progreso.
También puede ver el ejemplo completo en el repositorio dotnet/samples de GitHub. Para obtener instrucciones
de descarga, vea Ejemplos y tutoriales.
Requisitos previos
Deberá configurar la máquina para ejecutar .NET Core. Puede encontrar las instrucciones de instalación en la
página Descarga de .NET Core. Puede ejecutar esta aplicación en Windows, Ubuntu Linux, OS X o en un
contenedor de Docker. Deberá instalar su editor de código favorito. En las siguientes descripciones se usa Visual
Studio Code, que es un editor multiplataforma de código abierto. Sin embargo, puede usar las herramientas que
le resulten más cómodas.
Crear la aplicación
El primer paso es crear una nueva aplicación. Abra un símbolo del sistema y cree un nuevo directorio para la
aplicación. Conviértalo en el directorio actual. Escriba el comando dotnet new console en el símbolo del sistema.
Esta acción crea los archivos de inicio para una aplicación básica "Hola mundo".
Si nunca ha usado C# antes, en este tutorial se explica la estructura de un programa con C#. Puede leerlo y
después volver aquí para obtener más información sobre LINQ.
Si estas tres líneas (instrucciones using ) no se encuentran al principio del archivo, nuestro programa no se
compilará.
Ahora que tiene todas las referencias que necesitará, tenga en cuenta lo que constituye una baraja de cartas.
Habitualmente, una baraja de cartas tiene cuatro palos y cada palo tiene trece valores. Normalmente, podría
plantearse crear una clase Card directamente del archivo bat y rellenar manualmente una colección de objetos
Card . Con LINQ, puede ser más conciso que de costumbre al tratar con la creación de una baraja de cartas. En
lugar de crear una clase Card , puede crear dos secuencias para representar los palos y rangos,
respectivamente. Podrá crear un par sencillo de métodos iterator que generará las clasificaciones y palos como
objetos IEnumerable<T> de cadenas:
// Program.cs
// The Main() method
Colóquelos debajo del método Main en el archivo Program.cs . Estos dos métodos utilizan la sintaxis
yield return para generar una secuencia mientras se ejecutan. El compilador crea un objeto que implementa
IEnumerable<T> y genera la secuencia de cadenas conforme se solicitan.
Ahora, puede usar estos métodos iterator para crear la baraja de cartas. Insertará la consulta LINQ en nuestro
método Main . Aquí tiene una imagen:
// Program.cs
static void Main(string[] args)
{
var startingDeck = from s in Suits()
from r in Ranks()
select new { Suit = s, Rank = r };
// Display each card that we've generated and placed in startingDeck in the console
foreach (var card in startingDeck)
{
Console.WriteLine(card);
}
}
Las cláusulas múltiples from generan una salida SelectMany, que crea una única secuencia a partir de la
combinación de cada elemento de la primera secuencia con cada elemento de la segunda secuencia. El orden es
importante para nuestros propósitos. El primer elemento de la primera secuencia de origen (palos) se combina
con todos los elementos de la segunda secuencia (clasificaciones). Esto genera las trece cartas del primer palo.
Dicho proceso se repite con cada elemento de la primera secuencia (palos). El resultado final es una baraja de
cartas ordenadas por palos, seguidos de valores.
Es importante tener en cuenta que si decide escribir las instrucciones LINQ en la sintaxis de consulta usada
anteriormente o utilizar la sintaxis de método en su lugar, siempre es posible pasar de una forma de sintaxis a la
otra. La consulta anterior escrita en la sintaxis de consulta puede escribirse en la sintaxis de método como:
var startingDeck = Suits().SelectMany(suit => Ranks().Select(rank => new { Suit = suit, Rank = rank }));
El compilador convierte las instrucciones LINQ escritas en la sintaxis de consulta a la sintaxis de llamada de
método equivalente. Por consiguiente, independientemente de la sintaxis que prefiera, las dos versiones de la
consulta producen el mismo resultado. Elija la sintaxis más adecuada a su situación: por ejemplo, si trabaja en
un equipo en el que algunos de sus miembros tienen dificultades con la sintaxis de método, procure usar la
sintaxis de consulta.
Continúe y ejecute el ejemplo que se ha creado en este punto. Mostrará todas las 52 cartas de la baraja. Puede
ser muy útil ejecutar este ejemplo en un depurador para observar cómo se ejecutan los métodos Suits() y
Ranks() . Puede ver claramente que cada cadena de cada secuencia se genera solo según sea necesario.
// Program.cs
public static void Main(string[] args)
{
var startingDeck = from s in Suits()
from r in Ranks()
select new { Suit = s, Rank = r };
// 52 cards in a deck, so 52 / 2 = 26
var top = startingDeck.Take(26);
var bottom = startingDeck.Skip(26);
}
Pero no existe ningún método de orden aleatorio en la biblioteca estándar que pueda aprovechar, por lo que
tendrá que escribir el suyo propio. El método de orden aleatorio que cree mostrará varias técnicas que se
utilizan con programas basados en LINQ, por lo que cada parte de este proceso se explica en pasos.
Para agregar alguna funcionalidad a la forma de interactuar con el elemento IEnumerable<T> que obtendrá de
las consultas LINQ, tendrá que escribir algunos tipos especiales de métodos llamados métodos de extensión. En
resumen, un método de extensión es un método estático con una finalidad específica que agrega nueva
funcionalidad a un tipo existente sin tener que modificar el tipo original al que quiere agregar la funcionalidad.
Proporcione un nuevo espacio a sus métodos de extensión agregando un nuevo archivo de clase estática al
programa denominado Extensions.cs y comience a compilar el primer método de extensión:
// Extensions.cs
using System;
using System.Collections.Generic;
using System.Linq;
namespace LinqFaroShuffle
{
public static class Extensions
{
public static IEnumerable<T> InterleaveSequenceWith<T>(this IEnumerable<T> first, IEnumerable<T>
second)
{
// Your implementation will go here soon enough
}
}
}
Puede ver la incorporación del modificador this del primer argumento al método. Esto significa que se llama
al método como si fuese un método de miembro del tipo del primer argumento. Esta declaración de método
también sigue una expresión estándar donde los tipos de entrada y salida son IEnumerable<T> . Dicha práctica
permite que los métodos LINQ se encadenen entre sí para realizar consultas más complejas.
Naturalmente, dado que dividió la baraja en mitades, tendrá que unir esas mitades. En el código, esto significa
que enumerará las dos secuencias adquiridas a través de Take y Skip a la vez, interleaving los elementos y
crear una sola secuencia: su baraja de cartas recién ordenada aleatoriamente. Escribir un método LINQ que
funciona con dos secuencias requiere que comprenda cómo funciona IEnumerable<T>.
La interfaz IEnumerable<T> tiene un método: GetEnumerator. El objeto devuelto por GetEnumerator tiene un
método para desplazarse al siguiente elemento, así como una propiedad que recupera el elemento actual de la
secuencia. Utilizará estos dos miembros para enumerar la colección y devolver los elementos. Este método de
intercalación será un método iterador, por lo que en lugar de crear una colección y devolverla, usará la sintaxis
yield return anterior.
Ahora que ha escrito este método, vuelva al método Main y ordene la baraja aleatoriamente una vez:
// Program.cs
public static void Main(string[] args)
{
var startingDeck = from s in Suits()
from r in Ranks()
select new { Suit = s, Rank = r };
Comparaciones
¿Cuántos órdenes aleatorios se necesitan para devolver la baraja a su orden original? Para averiguarlo, debe
escribir un método que determine si dos secuencias son iguales. Cuando ya disponga del método, debe colocar
el código que ordena la baraja aleatoriamente en un bucle y comprobarlo para ver cuándo la baraja vuelve a
tener su orden original.
Debe ser sencillo escribir un método para determinar si las dos secuencias son iguales. Presenta una estructura
similar al método que se escribió para ordenar la baraja aleatoriamente. Solo que esta vez, en lugar de aplicar
yield return a cada elemento, se compararán los elementos coincidentes de cada secuencia. Después de que
se haya enumerado la secuencia completa, si cada elemento coincide, las secuencias son las mismas:
return true;
}
Esto muestra una segunda expresión LINQ: los métodos de terminal. Adoptan una secuencia como entrada (o,
en este caso, dos secuencias) y devuelven un único valor escalar. Cuando se utilizan métodos de terminal,
siempre son el método final en una cadena de métodos para una consulta LINQ, de ahí el nombre "terminal".
Puede ver esto en acción cuando lo usa para determinar cuándo la baraja vuelve a tener su orden original.
Coloque el código de orden aleatorio dentro de un bucle y deténgalo cuando la secuencia vuelva a su orden
original, mediante la aplicación del método SequenceEquals() . Puede observar que siempre se tratará del
método final de cualquier consulta, porque devuelve un único valor en lugar de una secuencia:
// Program.cs
static void Main(string[] args)
{
// Query for building the deck
var times = 0;
// We can re-use the shuffle variable from earlier, or you can make a new one
shuffle = startingDeck;
do
{
shuffle = shuffle.Take(26).InterleaveSequenceWith(shuffle.Skip(26));
} while (!startingDeck.SequenceEquals(shuffle));
Console.WriteLine(times);
}
Ejecute el código que tenga hasta el momento y tome nota de cómo la baraja se reorganiza en cada orden
aleatorio. Después de ocho órdenes aleatorios (iteraciones del bucle do-while), la baraja vuelve a la
configuración original en que se encontraba cuando la creó a partir la consulta LINQ inicial.
Optimizaciones
El ejemplo creado hasta el momento se ejecuta en orden no aleatorio, donde las cartas superiores e inferiores
son las mismas en cada ejecución. Vamos a realizar un cambio: utilizaremos una ejecución en orden aleatorio en
su lugar, donde las 52 cartas cambian de posición. Si se trata de un orden aleatorio, intercale la baraja de tal
forma que la primera carta de la mitad inferior sea la primera carta de la baraja. Esto significa que la última
carta de la mitad superior será la carta inferior. Se trata de un cambio simple en una única línea de código.
Actualice la consulta de orden aleatorio actual cambiando las posiciones de Take y Skip. Se cambiará al orden de
las mitades superior e inferior de la baraja:
shuffle = shuffle.Skip(26).InterleaveSequenceWith(shuffle.Take(26));
Vuelva a ejecutar el programa y verá que, para que la baraja se reordene, se necesitan 52 iteraciones. También
empezará a observar algunas degradaciones graves de rendimiento a medida que el programa continúa en
ejecución.
Esto se debe a varias razones. Puede que se trate de una de las principales causas de este descenso de
rendimiento: un uso ineficaz de la evaluación diferida.
En pocas palabras, la evaluación diferida indica que no se realiza la evaluación de una instrucción hasta que su
valor es necesario. Las consultas LINQ son instrucciones se evalúan de forma diferida. Las secuencias se
generan solo a medida que se solicitan los elementos. Normalmente, es una ventaja importante de LINQ. Sin
embargo, en un uso como el que hace este programa, se produce un aumento exponencial del tiempo de
ejecución.
Recuerde que la baraja original se generó con una consulta LINQ. Cada orden aleatorio se genera mediante la
realización de tres consultas LINQ sobre la baraja anterior. Todas se realizan de forma diferida. Eso también
significa que se vuelven a llevar a cabo cada vez que se solicita la secuencia. Cuando llegue a la iteración
número 52, habrá regenerado la baraja original demasiadas veces. Se va a escribir un registro para mostrar este
comportamiento. A continuación, podrá corregirlo.
En su archivo Extensions.cs , escriba o copie el siguiente método. Este método de extensión crea otro archivo
denominado debug.log en el directorio del proyecto y registra la consulta que se ejecuta actualmente en el
archivo de registro. Este método de extensión se puede anexar a una consulta para marcar que se ha ejecutado.
return sequence;
}
Verá un subrayado en zigzag rojo bajo File , lo que indica que no existe. No se compilará, ya que el compilador
no sabe qué es File . Para solucionar este problema, asegúrese de agregar la siguiente línea de código bajo la
primera línea de Extensions.cs :
using System.IO;
Console.WriteLine();
var times = 0;
var shuffle = startingDeck;
do
{
// Out shuffle
/*
shuffle = shuffle.Take(26)
.LogQuery("Top Half")
.InterleaveSequenceWith(shuffle.Skip(26)
.LogQuery("Bottom Half"))
.LogQuery("Shuffle");
*/
// In shuffle
shuffle = shuffle.Skip(26).LogQuery("Bottom Half")
.InterleaveSequenceWith(shuffle.Take(26).LogQuery("Top Half"))
.LogQuery("Shuffle");
times++;
Console.WriteLine(times);
} while (!startingDeck.SequenceEquals(shuffle));
Console.WriteLine(times);
}
Observe que no se genera un registro cada vez que accede a una consulta. El registro solo se genera cuando
crea la consulta original. El programa todavía tarda mucho tiempo en ejecutarse, pero ahora puede ver por qué.
Si se le agota la paciencia al ejecutar el orden aleatorio interno con los registros activados, vuelva al orden
aleatorio externo. Aún puede ver los efectos de la evaluación diferida. En una ejecución, ejecuta 2592 consultas,
incluida toda la generación de palos y valores.
Aquí puede mejorar el rendimiento del código para reducir el número de ejecuciones que realiza. Una
corrección sencilla que puede hacer es almacenar en caché los resultados de la consulta LINQ original que
construye la baraja de cartas. Actualmente, ejecuta las consultas una y otra vez siempre que el bucle do-while
pasa por una iteración, lo que vuelve a construir la baraja de cartas y cambia continuamente el orden aleatorio.
Para almacenar en caché la baraja de cartas, puede aprovechar los métodos LINQ ToArray y ToList; cuando los
anexe a las consultas, realizarán las mismas acciones que les ha indicado que hagan, pero ahora almacenarán
los resultados en una matriz o una lista, según el método al que elija llamar. Anexe el método ToArray de LINQ a
las dos consultas y ejecute de nuevo el programa:
public static void Main(string[] args)
{
var startingDeck = (from s in Suits().LogQuery("Suit Generation")
from r in Ranks().LogQuery("Value Generation")
select new { Suit = s, Rank = r })
.LogQuery("Starting Deck")
.ToArray();
Console.WriteLine();
var times = 0;
var shuffle = startingDeck;
do
{
/*
shuffle = shuffle.Take(26)
.LogQuery("Top Half")
.InterleaveSequenceWith(shuffle.Skip(26).LogQuery("Bottom Half"))
.LogQuery("Shuffle")
.ToArray();
*/
shuffle = shuffle.Skip(26)
.LogQuery("Bottom Half")
.InterleaveSequenceWith(shuffle.Take(26).LogQuery("Top Half"))
.LogQuery("Shuffle")
.ToArray();
times++;
Console.WriteLine(times);
} while (!startingDeck.SequenceEquals(shuffle));
Console.WriteLine(times);
}
Ahora el orden aleatorio externo se reduce a 30 consultas. Vuelva a ejecutarlo con el orden aleatorio interno y
verá mejoras similares: ahora ejecuta 162 consultas.
Este ejemplo está diseñado para resaltar los casos de uso en que la evaluación diferida puede generar
dificultades de rendimiento. Si bien es importante ver dónde la evaluación diferida puede afectar al rendimiento
del código, es igualmente importante entender que no todas las consultas deben ejecutarse de manera
diligente. El resultado de rendimiento en el que incurre sin usar ToArray se debe a que cada nueva disposición
de la baraja de cartas se crea a partir de la disposición anterior. La evaluación diferida supone que cada nueva
configuración de la baraja se realiza a partir de la baraja original, incluso con la ejecución del código que crea el
elemento startingDeck . Esto conlleva una gran cantidad de trabajo adicional.
En la práctica, algunos algoritmos se ejecutan bien con la evaluación diligente y otros, con la evaluación diferida.
Para el uso diario, la evaluación diferida suele ser una mejor opción cuando el origen de datos es un proceso
independiente, como un motor de base de datos. Para las bases de datos, la evaluación diferida permite realizar
consultas más complejas que ejecuten un solo recorrido de ida y vuelta al procesamiento de la base de datos y
vuelvan al resto del código. LINQ es flexible tanto si decide usar la evaluación diligente como la diferida, así que
calibre sus procesos y elija el tipo de evaluación que le ofrece el mejor rendimiento.
Conclusión
En este proyecto ha tratado lo siguiente:
Uso de consultas LINQ para agregar datos a una secuencia significativa
Escritura de métodos de extensión para agregar nuestra propia funcionalidad personalizada a las consultas
LINQ
Localización de áreas en nuestro código donde nuestras consultas LINQ pueden tener problemas de
rendimiento como la degradación de la velocidad
Evaluación diligente y diferida en lo que respecta a las consultas LINQ y las implicaciones que podrían tener
en el rendimiento de la consulta
Aparte de LINQ, ha aprendido algo sobre una técnica que los magos utilizan para hacer trucos de cartas. Los
magos usan el orden aleatorio Faro porque les permite controlar dónde está cada carta en la baraja. Ahora que
lo conoce, no se lo estropee a los demás.
Para más información sobre LINQ, vea:
Language-Integrated Query (LINQ)
Introducción a LINQ
Operaciones básicas de consulta LINQ (C#)
Transformaciones de datos con LINQ (C#)
Sintaxis de consultas y sintaxis de métodos en LINQ (C#)
Características de C# compatibles con LINQ
Uso de atributos en C#
16/09/2021 • 7 minutes to read
Los atributos proporcionan una manera de asociar la información con el código de manera declarativa. También
pueden proporcionar un elemento reutilizable que se puede aplicar a diversos destinos.
Considere el atributo [Obsolete] . Se puede aplicar a clases, structs, métodos, constructores y más. Declara que
el elemento está obsoleto. Es decisión del compilador de C# buscar este atributo y realizar alguna acción como
respuesta.
En este tutorial, se le introducirá a cómo agregar atributos al código, cómo crear y usar sus propios atributos y
cómo usar algunos atributos que se integran en .NET Core.
Requisitos previos
Deberá configurar la máquina para ejecutar .NET Core. Puede encontrar las instrucciones de instalación en la
página Descargas de .NET Core. Puede ejecutar esta aplicación en Windows, Ubuntu Linux, macOS o en un
contenedor de Docker. Deberá instalar su editor de código favorito. En las siguientes descripciones se usa Visual
Studio Code, que es un editor multiplataforma de código abierto. Sin embargo, puede usar las herramientas que
le resulten más cómodas.
Crear la aplicación
Ahora que ha instalado todas las herramientas, cree una nueva aplicación de .NET Core. Para usar el generador
de línea de comandos, ejecute el siguiente comando en su shell favorito:
dotnet new console
Este comando creará archivos de proyecto esenciales de .NET Core. Debe ejecutar dotnet restore para
restaurar las dependencias necesarias para compilar este proyecto.
No es necesario ejecutar dotnet restore porque lo ejecutan implícitamente todos los comandos que necesitan
que se produzca una restauración, como dotnet new , dotnet build , dotnet run , dotnet test , dotnet publish
y dotnet pack . Para deshabilitar la restauración implícita, use la opción --no-restore .
El comando dotnet restore sigue siendo válido en algunos escenarios donde tiene sentido realizar una
restauración explícita, como las compilaciones de integración continua en Azure DevOps Services o en los
sistemas de compilación que necesitan controlar explícitamente cuándo se produce la restauración.
Para obtener información sobre cómo administrar fuentes de NuGet, vea la documentación de dotnet restore .
Para ejecutar el programa, use dotnet run . Deberá ver la salida "Hola a todos" a la consola.
Tenga en cuenta que, mientras que la clase se denomina ObsoleteAttribute , solo es necesario usar [Obsolete]
en el código. Se trata de una convención que sigue C#. Puede usar el nombre completo [ObsoleteAttribute] si
así lo prefiere.
Cuando se marca una clase como obsoleta, es una buena idea proporcionar alguna información de por qué es
obsoleta, o qué usar en su lugar. Para ello se pasa un parámetro de cadena al atributo obsoleto.
Con lo anterior, ahora puedo usar [MySpecial] (o [MySpecialAttribute] ) como un atributo en otra parte de la
base de código.
[MySpecial]
public class SomeOtherClass
{
}
Los atributos de la biblioteca de clases base de .NET como ObsoleteAttribute desencadenan ciertos
comportamientos en el compilador. Sin embargo, cualquier atributo que cree funcionará como metadatos y no
tendrá como resultado ningún código dentro de la clase de atributo que se ejecuta. Es decisión suya actuar
sobre esos metadatos en otra parte del código (más adelante en este tutorial se hablará sobre ello).
Aquí hay un problema que se debe vigilar. Como se mencionó anteriormente, solo determinados tipos se
pueden pasar como argumentos al usar atributos. Sin embargo, al crear un tipo de atributo, el compilador de C#
no le impedirá crear esos parámetros. En el ejemplo siguiente, he creado un atributo con un constructor que se
compila correctamente.
public class GotchaAttribute : Attribute
{
public GotchaAttribute(Foo myClass, string str) {
}
}
Sin embargo, no podrá usar este constructor con una sintaxis de atributo.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class MyAttributeForClassAndStructOnly : Attribute
{
}
Si intenta colocar el atributo anterior en algo que no sea una clase o un struct, obtendrá un error del compilador
como
Attribute 'MyAttributeForClassAndStructOnly' is not valid on this declaration type. It is only valid on
'class, struct' declarations
.
public class Foo
{
// if the below attribute was uncommented, it would cause a compiler error
// [MyAttributeForClassAndStructOnly]
public Foo()
{ }
}
Este es un ejemplo del uso de GetCustomAttributes en una instancia de MemberInfo para MyClass (que
anteriormente vimos que contiene un atributo [Obsolete] ).
Eso se imprimirá en la consola: Attribute on MyClass: ObsoleteAttribute . Intente agregar otros atributos a
MyClass .
Es importante tener en cuenta que se crean instancias de estos objetos Attribute de forma diferida. Es decir, no
podrá crea una instancia de ellos hasta que use GetCustomAttribute o GetCustomAttributes . También se crea
una instancia de ellos cada vez. Al llamar a GetCustomAttributes dos veces en una fila se devuelven dos
instancias diferentes de ObsoleteAttribute .
En el código anterior, no necesita tener una cadena "Name" de literal. Esto puede ayudar a impedir errores
relacionados con los tipos y también agiliza los procesos de refactorización o cambio de nombre.
Resumen
Los atributos traen la eficacia declarativa a C#, pero son una forma de metadatos de código y no actúan por sí
mismos.
Tipos de referencia que aceptan valores NULL
16/09/2021 • 11 minutes to read
Una de las novedades de C# 8.0 son los tipos de referencia que aceptan valores NULL y los tipos de
referencia que no aceptan valores NULL , que permiten usar instrucciones importantes sobre las
propiedades de variables de tipos de referencia:
Se supone que una referencia no debe ser NULL . Si las variables no deben ser NULL, el compilador
aplica reglas que garantizan que sea seguro desreferenciar dichas variables sin comprobar primero que no
se trata de un valor NULL:
La variable debe inicializarse como un valor distinto a NULL.
No se puede asignar el valor null a la variable.
Una referencia puede ser NULL . Si las variables pueden ser NULL, el compilador aplica reglas para
garantizar que haya comprobado correctamente si hay referencias NULL:
Solo se puede desreferenciar la variable si el compilador pueda garantizar que el valor no sea NULL.
Estas variables se pueden inicializar con el valor null predeterminado, y se les puede asignar el valor
null en otro código.
Esta nueva característica proporciona grandes ventajas sobre el control de variables de referencia con respecto a
versiones anteriores de C#, donde la intención del diseño no se puede determinar a partir de la declaración de la
variable. El compilador no proporcionaba protección contra excepciones de referencia NULL para los tipos de
referencia:
Una referencia puede ser NULL . El compilador no emite ninguna advertencia cuando una variable de
tipo de referencia se inicializa con null o posteriormente se le asigna null . El compilador emite
advertencias cuando estas variables se desreferencian sin comprobaciones de valores NULL.
Se asume que una referencia no es NULL . Si se desreferencian tipos de referencia, el compilador no
emite ninguna advertencia. El compilador emite advertencias si una variable se establece en una expresión
que puede ser NULL.
Estas advertencias se emiten en tiempo de compilación. El compilador no agrega comprobaciones de valores
NULL ni otras construcciones de tiempo de ejecución en un contexto que admite un valor NULL. En tiempo de
ejecución, una referencia que acepta valores NULL y una referencia que no acepta valores NULL son
equivalentes.
Con la incorporación de los tipos de referencia que aceptan valores NULL, puede declarar su intención de forma
más clara. El valor null es la forma adecuada de representar que una variable que no hace referencia a un
valor. No use esta característica para eliminar todos los valores null de su código. En su lugar, debería declarar
su intención al compilador para que los demás desarrolladores la puedan ver al leer el código. Al declarar su
intención, el compilador le informa de cuándo escribe código que no es coherente con esa intención.
Un tipo de referencia que acepta valores NULL se anota con la misma sintaxis que los tipos de valor que
aceptan valores NULL: se agrega ? junto al tipo de la variable. Por ejemplo, la siguiente declaración de variable
representa una variable de cadena que acepta valores NULL, name :
string? name;
Cualquier variable en la que ? no se anexe al nombre de tipo es un tipo de referencia que no acepta
valores NULL . Esto incluye todas las variables de tipo de referencia en el código existente en el momento en el
que se habilita esta característica.
El compilador usa el análisis estático para determinar si se sabe si una referencia que acepta valores NULL no
tiene este tipo de valor. Si desreferencia una referencia que acepta valores NULL cuando esta puede ser NULL, el
compilador genera una advertencia. Puede invalidar este comportamiento con el operador de limitación de
advertencias de valores NULL ! después del nombre de una variable. Por ejemplo, si sabe que la variable name
no es NULL, pero el compilador genera una advertencia, puede escribir el código siguiente para invalidar el
análisis del compilador:
name!.Length;
Nulabilidad de tipos
Cualquier tipo de referencia puede tener una de cuatro nulabilidades, que describen cuándo se generan las
advertencias:
No acepta valores NULL: no se pueden asignar valores NULL a las variables de este tipo. No es necesario
comprobar si estas tienen un valor NULL antes de desreferenciarlas.
Acepta valores NULL: se pueden asignar valores NULL a las variables de este tipo. Si se desreferencian sin
comprobar primero la existencia de valores null , se producirá una advertencia.
Inconsciente: se trata del estado previo a C# 8.0. Las variables de este tipo se pueden desreferenciar o asignar
sin advertencias.
Desconocido: este es generalmente el caso de los parámetros de tipo en los que las restricciones no indican
al compilador que el tipo debe aceptar valores NULL o no aceptar valores NULL.
La nulabilidad de un tipo en una declaración de variable se controla mediante el contexto que acepta valores
NULL en el que se declara la variable.
<Nullable>enable</Nullable>
También puede usar directivas para establecer los mismos contextos en cualquier lugar del proyecto:
#nullable enable : establece el contexto de anotación que acepta valores NULL y el contexto de advertencia
que acepta valores NULL en enabled .
#nullable disable : establece el contexto de anotación que acepta valores NULL y el contexto de advertencia
que acepta valores NULL en disabled .
#nullable restore : restaura el contexto de anotación que acepta valores NULL y el contexto de advertencia
que acepta valores NULL según la configuración del proyecto.
#nullable disable warnings : establece el contexto de advertencia que acepta valores NULL en disabled .
#nullable enable warnings : establece el contexto de advertencia que acepta valores NULL en enabled .
#nullable restore warnings : restaura el contexto de advertencia que acepta valores NULL según la
configuración del proyecto.
#nullable disable annotations : establezca el contexto de anotación que admite un valor NULL en disabled .
#nullable enable annotations : establezca el contexto de anotación que admite un valor NULL en enabled .
#nullable restore annotations : restaura el contexto de advertencia de anotación según la configuración del
proyecto.
IMPORTANT
El contexto global que admite un valor NULL no se aplica a los archivos de código generado. En cualquier estrategia, el
contexto que admite un valor NULL está deshabilitado para cualquier archivo de código fuente marcado como generado.
Esto significa que las API de los archivos generados no se anotan. Hay cuatro maneras de marcar un archivo como
generado:
1. En el archivo .editorconfig, especifique generated_code = true en una sección que se aplique a ese archivo.
2. Coloque <auto-generated> o <auto-generated/> en un comentario en la parte superior del archivo. Puede estar
en cualquier línea de ese comentario, pero el bloque de comentario debe ser el primer elemento del archivo.
3. Inicie el nombre de archivo con TemporaryGeneratedFile_
4. Finalice el nombre de archivo con .designer.cs, .generated.cs, .g.cs o .g.i.cs.
Los generadores pueden optar por usar la directiva de preprocesador #nullable .
De forma predeterminada, los contextos de advertencias y anotaciones que aceptan valores NULL están
deshabilitados . Esto implica que el código existente se compila sin cambios y sin generar ninguna advertencia
nueva.
Estas opciones proporcionan dos estrategias distintas para actualizar un código base existente para usar tipos
de referencia que aceptan valores NULL.
Problemas conocidos
Las matrices y las estructuras que contienen tipos de referencia son problemas conocidos de la característica de
tipos de referencia que aceptan valores NULL.
Estructuras
Una estructura que contiene tipos de referencia que no aceptan valores NULL permite asignarle default sin
ninguna advertencia. Considere el ejemplo siguiente:
using System;
#nullable enable
En el ejemplo anterior, no hay ninguna advertencia en PrintStudent(default) mientras que los tipos de
referencia que no aceptan valores NULL FirstName y LastName son NULL.
Otro caso más común es cuando se trata de estructuras genéricas. Considere el ejemplo siguiente:
#nullable enable
En el ejemplo anterior, la propiedad Bar será null en tiempo de ejecución y se asigna a una cadena que no
acepta valores NULL sin ninguna advertencia.
Matrices
Las matrices también son un problema conocido en los tipos de referencia que aceptan valores NULL. Considere
el ejemplo siguiente, que no genera ninguna advertencia:
using System;
#nullable enable
En el ejemplo anterior, la declaración de la matriz muestra que contiene cadenas que no aceptan valores NULL,
mientras que todos sus elementos se inicializan en NULL. Después, a la variable s se le asigna un valor NULL
(el primer elemento de la matriz). Por último, se desreferencia la variable s , lo que genera una excepción en
tiempo de ejecución.
Vea también
Borrador de especificación de tipos de referencia que aceptan valores NULL
Tutorial de introducción a las referencias que no aceptan valores NULL
Migración de un código base existente a referencias que aceptan valores NULL
Nullable (opción del compilador de C#)
Actualización de las bibliotecas para usar tipos de
referencia que aceptan valores NULL y comunicar
reglas que aceptan valores NULL a los llamadores
16/09/2021 • 12 minutes to read
La adición de tipos de referencia que aceptan valores NULL significa que puede declarar si se permite o espera
un valor de null para cada variable. Además, puede aplicar varios atributos, AllowNull , DisallowNull ,
MaybeNull , NotNull , NotNullWhen , MaybeNullWhen y NotNullIfNotNull , para describir por completo los estados
NULL de los valores devueltos y de argumento. Esto proporciona una gran experiencia de escritura de código.
Por ejemplo, obtiene advertencias si una variable que no acepta valores NULL está establecida en null .
También recibe advertencias si no se comprueban los valores NULL de una variable que acepta valores NULL
antes de desreferenciarla. La actualización de las bibliotecas puede llevar un tiempo, pero merece la pena.
Cuanta más información proporcione al compilador sobre cuándo está permitido o prohibido un valor null ,
mejores advertencias obtendrán los usuarios de la API. Para empezar, se usará un ejemplo conocido. Imagine
que la biblioteca tiene la siguiente API para recuperar una cadena de recursos:
En el ejemplo anterior se sigue el conocido patrón de Try* en .NET. Hay dos argumentos de referencia para
esta API: los parámetros key y message . Esta API tiene las siguientes reglas relacionadas con la obtención del
valor NULL de estos argumentos:
Los autores de la llamada no deben pasar null como argumento para key .
Los autores de la llamada pueden pasar una variable cuyo valor sea null como argumento de message .
Si el método TryGetMessage devuelve true , el valor de message no es NULL. Si el valor devuelto es false, ,
el valor de message (y su estado NULL) es NULL.
La regla para key se puede expresar completamente mediante el tipo de variable; key debe ser un tipo de
referencia que no acepte valores NULL. El parámetro message es más complejo. Permite null como
argumento, pero garantiza que, si se ejecuta correctamente, el argumento out no sea NULL. En estos
escenarios, necesita un vocabulario más completo para describir las expectativas.
La actualización de la biblioteca para las referencias que aceptan valores NULL requiere más trabajo que
agregar ? en algunas de las variables y los nombres de tipo. En el ejemplo anterior se muestra que debe
examinar las API y tener en cuenta las expectativas para cada argumento de entrada. Considere las garantías
para el valor devuelto y cualquier argumento out o ref en la devolución del método. Después, comunique
dichas reglas al compilador y este proporcionará advertencias cuando los llamadores no las cumplan.
Este trabajo lleva tiempo. Empecemos con las estrategias para hacer que su biblioteca o aplicación sea
compatible con valores NULL, a la vez que equilibra otros requisitos. Verá cómo equilibrar el desarrollo continuo
habilitando tipos de referencia que admiten valores NULL. Conocerá los desafíos de las definiciones de tipo
genérico. Y aprenderá a aplicar atributos para describir condiciones previas y posteriores en las API individuales.
class Student
{
[Required]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
class Student
{
[Required]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
Para este DTO, la única propiedad que puede ser NULL es VehicleRegistration .
A pesar de esto, el compilador genera advertencias CS8618 para FirstName y LastName para indicar que las
propiedades que no aceptan valores NULL no están inicializadas.
Hay tres opciones disponibles para resolver las advertencias del compilador de una manera que mantiene la
intención original. Cualquiera de estas opciones es válida y debe elegir la que mejor se adapte a sus requisitos
de diseño y su estilo de codificación.
Inicializar en el constructor
La forma ideal de resolver las advertencias de propiedades no inicializadas es inicializar las propiedades en el
constructor:
class Student
{
public Student(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
[Required]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
Este enfoque únicamente funciona si la biblioteca que se usa para crear instancias de la clase admite el paso de
parámetros en el constructor.
Una biblioteca puede admitir el paso de algunas propiedades en el constructor, pero no todas. Por ejemplo,
EF Core admite el enlace del constructor para las propiedades de columna normales, pero no para las
propiedades de navegación.
Consulte la documentación de la biblioteca que crea instancias de la clase para comprender en qué medida
admite el enlace del constructor.
Propiedad con campo de respaldo que acepta valores NULL
Si el enlace del constructor no funcionara en su caso, una manera de solucionar este problema sería tener una
propiedad que no acepta valores NULL con un campo de respaldo que acepta valores NULL:
[Required]
public string FirstName
{
set => _firstName = value;
get => _firstName
?? throw new InvalidOperationException("Uninitialized " + nameof(FirstName))
}
En este escenario, si se accede a la propiedad FirstName antes de que se haya inicializado, el código produce
una excepción InvalidOperationException , porque el contrato de API se ha usado incorrectamente.
Tenga en cuenta que algunas bibliotecas pueden tener requisitos especiales a la hora de usar los campos de
respaldo. Por ejemplo, puede ser necesario configurar EF Core para usar los campos de respaldo correctamente.
Inicialización de la propiedad con un valor NULL
Como alternativa al uso de un campo de respaldo que acepta valores NULL, o si la biblioteca que crea instancias
de la clase no es compatible con ese enfoque, puede inicializar la propiedad como null directamente con la
ayuda del operador que permite valores NULL ( ! ):
[Required]
public string FirstName { get; set; } = null!;
[Required]
public string LastName { get; set; } = null!;
Nunca observará un valor NULL real en tiempo de ejecución, a menos que se produzca un error de
programación al acceder a la propiedad antes de que se haya inicializado correctamente.
Consulte también
Migración de un código base existente a referencias que aceptan valores NULL
Uso de tipos de referencia que aceptan valores NULL en EF Core
Métodos de C#
16/09/2021 • 22 minutes to read
Un método es un bloque de código que contiene una serie de instrucciones. Un programa hace que se ejecuten
las instrucciones al llamar al método y especificando los argumentos de método necesarios. En C#, todas las
instrucciones ejecutadas se realizan en el contexto de un método. El método Main es el punto de entrada para
cada aplicación de C# y se llama mediante Common Language Runtime (CLR) cuando se inicia el programa.
NOTE
En este tema se analizan los métodos denominados. Para obtener información sobre las funciones anónimas, vea
Funciones anónimas.
Firmas de método
Los métodos se declaran en un elemento class , record o struct al especificar lo siguiente:
Un nivel de acceso opcional, como, por ejemplo, public o private . De manera predeterminada, es private
.
Modificadores opcionales, como, por ejemplo, abstract o sealed .
El valor devuelto o, si el método no tiene ninguno, void .
El nombre del método.
Los parámetros del método. Los parámetros de método se encierran entre paréntesis y se separan por
comas. Los paréntesis vacíos indican que el método no requiere parámetros.
Todas estas partes forman la firma del método.
IMPORTANT
Un tipo de valor devuelto de un método no forma parte de la firma del método con el objetivo de sobrecargar el método.
Sin embargo, forma parte de la firma del método al determinar la compatibilidad entre un delegado y el método que
señala.
En el siguiente ejemplo se define una clase denominada Motorcycle que contiene cinco métodos:
using System;
Tenga en cuenta que la clase Motorcycle incluye un método sobrecargado, Drive . Dos métodos tienen el
mismo nombre, pero se deben diferenciar en sus tipos de parámetros.
Invocación de método
Los métodos pueden ser de instancia o estáticos. Para invocar un método de instancia es necesario crear una
instancia de un objeto y llamar al método del objeto; el método de una instancia actúa en dicha instancia y sus
datos. Si quiere invocar un método estático, haga referencia al nombre del tipo al que pertenece el método; los
métodos estáticos no actúan en datos de instancia. Al intentar llamar a un método estático mediante una
instancia de objeto se genera un error del compilador.
Llamar a un método es como acceder a un campo. Después del nombre de objeto (si se llama a un método de
instancia) o el nombre de tipo (si llama a un método static ), agregue un punto, el nombre del método y
paréntesis. Los argumentos se enumeran entre paréntesis y se separan mediante comas.
La definición del método especifica los nombres y tipos de todos los parámetros necesarios. Cuando un autor
de llamada invoca el método, proporciona valores concretos denominados argumentos para cada parámetro.
Los argumentos deben ser compatibles con el tipo de parámetro, pero el nombre de argumento, si se usa
alguno en el código de llamada, no tiene que ser el mismo que el del parámetro con nombre definido en el
método. En el ejemplo siguiente, el método Square incluye un parámetro único de tipo int denominado i. La
primera llamada de método pasa al método Square una variable de tipo int denominada num; la segunda,
una constante numérica; y la tercera, una expresión.
public class Example
{
public static void Main()
{
// Call with an int variable.
int num = 4;
int productA = Square(num);
La forma más común de invocación de método usa argumentos posicionales; proporciona argumentos en el
mismo orden que los parámetros de método. Los métodos de la clase Motorcycle se pueden llamar como en el
ejemplo siguiente. Por ejemplo, la llamada al método Drive incluye dos argumentos que se corresponden con
los dos parámetros de la sintaxis del método. El primero se convierte en el valor del parámetro miles y el
segundo en el valor del parámetro speed .
moto.StartEngine();
moto.AddGas(15);
moto.Drive(5, 20);
double speed = moto.GetTopSpeed();
Console.WriteLine("My top speed is {0}", speed);
}
}
También se pueden usar argumentos con nombre en lugar de argumentos posicionales al invocar un método.
Cuando se usan argumentos con nombre, el nombre del parámetro se especifica seguido de dos puntos (":") y el
argumento. Los argumentos del método pueden aparecer en cualquier orden, siempre que todos los
argumentos necesarios están presentes. En el ejemplo siguiente se usan argumentos con nombre para invocar
el método TestMotorcycle.Drive . En este ejemplo, los argumentos con nombre se pasan en orden inverso desde
la lista de parámetros del método.
using System;
Un método se puede invocar con argumentos posicionales y argumentos con nombre. Pero los argumentos con
nombre solo pueden ir detrás de argumentos posicionales si están en la posición correcta. En el ejemplo
siguiente se invoca el método TestMotorcycle.Drive del ejemplo anterior con un argumento posicional y un
argumento con nombre.
Los tipos pueden invalidar miembros heredados usando la palabra clave override y proporcionando una
implementación para el método invalidado. La firma del método debe ser igual a la del método invalidado. El
ejemplo siguiente es similar al anterior, salvo que invalida el método Equals(Object). (También invalida el
método GetHashCode(), ya que los dos métodos están diseñados para proporcionar resultados coherentes).
using System;
Pasar parámetros
Todos los tipos de C# son tipos de valor o tipos de referencia. Para obtener una lista de tipos de valor integrados,
vea Tipos. De forma predeterminada, los tipos de valor y los tipos de referencia se pasan a un método por valor.
using System;
Cuando un objeto de un tipo de referencia se pasa a un método por valor, se pasa por valor una referencia al
objeto. Es decir, el método no recibe el objeto concreto, sino un argumento que indica la ubicación del objeto. Si
cambia un miembro del objeto mediante esta referencia, el cambio se reflejará en el objeto cuando el control
vuelva al método de llamada. Pero el reemplazo del objeto pasado al método no tendrá ningún efecto en el
objeto original cuando el control vuelva al autor de la llamada.
En el ejemplo siguiente se define una clase (que es un tipo de referencia) denominada SampleRefType . Crea una
instancia de un objeto SampleRefType , asigna 44 a su campo value y pasa el objeto al método ModifyObject .
Fundamentalmente, este ejemplo hace lo mismo que el ejemplo anterior: pasa un argumento por valor a un
método. Pero, debido a que se usa un tipo de referencia, el resultado es diferente. La modificación que se lleva a
cabo en ModifyObject para el campo obj.value cambia también el campo value del argumento, rt , en el
método Main a 33, tal y como muestra el resultado del ejemplo.
using System;
using System;
Un patrón común que se usa en parámetros ref implica intercambiar los valores de variables. Se pasan dos
variables a un método por referencia y el método intercambia su contenido. En el ejemplo siguiente se
intercambian valores enteros.
using System;
Pasar un parámetro de tipo de referencia le permite cambiar el valor de la propia referencia, en lugar del valor
de sus campos o elementos individuales.
Matrices de parámetros
A veces, el requisito de especificar el número exacto de argumentos al método es restrictivo. El uso de la palabra
clave params para indicar que un parámetro es una matriz de parámetros permite llamar al método con un
número variable de argumentos. El parámetro etiquetado con la palabra clave params debe ser un tipo de
matriz y ser el último parámetro en la lista de parámetros del método.
Un autor de llamada puede luego invocar el método de una de las tres maneras siguientes:
Si se pasa una matriz del tipo adecuado que contenga el número de elementos que se quiera.
Si se pasa una lista separada por comas de los argumentos individuales del tipo adecuado para el método.
Pasando null .
Si no se proporciona un argumento a la matriz de parámetros.
En el ejemplo siguiente se define un método denominado GetVowels que devuelve todas las vocales de una
matriz de parámetros. El método Main muestra las cuatro formas de invocar el método. Los autores de
llamadas no deben proporcionar argumentos para los parámetros que incluyen el modificador params . En ese
caso, el parámetro es una matriz vacía.
using System;
using System.Linq;
class Example
{
static void Main()
{
string fromArray = GetVowels(new[] { "apple", "banana", "pear" });
Console.WriteLine($"Vowels from array: '{fromArray}'");
Si un método incluye parámetros necesarios y opcionales, los parámetros opcionales se definen al final de la
lista de parámetros, después de todos los parámetros necesarios.
En el ejemplo siguiente se define un método, ExampleMethod , que tiene un parámetro necesario y dos
opcionales.
using System;
Si se invoca un método con varios argumentos opcionales mediante argumentos posicionales, el autor de la
llamada debe proporcionar un argumento para todos los parámetros opcionales, del primero al último, a los
que se proporcione un argumento. Por ejemplo, en el caso del método ExampleMethod , si el autor de la llamada
proporciona un argumento para el parámetro description , también debe proporcionar uno para el parámetro
optionalInt . opt.ExampleMethod(2, 2, "Addition of 2 and 2"); es una llamada de método válida;
opt.ExampleMethod(2, , "Addition of 2 and 0"); genera un error del compilador, "Falta un argumento".
Si se llama a un método mediante argumentos con nombre o una combinación de argumentos posicionales y
con nombre, el autor de la llamada puede omitir los argumentos que siguen al último argumento posicional en
la llamada al método.
En el ejemplo siguiente se llama tres veces al método ExampleMethod . Las dos primeras llamadas al método
usan argumentos posicionales. La primera omite los dos argumentos opcionales, mientras que la segunda
omite el último argumento. La tercera llamada de método proporciona un argumento posicional para el
parámetro necesario, pero usa un argumento con nombre para proporcionar un valor al parámetro
description mientras omite el argumento optionalInt .
class SimpleMath
{
public int AddTwoNumbers(int number1, int number2)
{
return number1 + number2;
}
Para utilizar un valor devuelto de un método, el método de llamada puede usar la llamada de método en
cualquier lugar; un valor del mismo tipo sería suficiente. También puede asignar el valor devuelto a una variable.
Por ejemplo, los dos siguientes ejemplos de código logran el mismo objetivo:
Usar una variable local, en este caso, result , para almacenar un valor es opcional. La legibilidad del código
puede ser útil, o puede ser necesaria si debe almacenar el valor original del argumento para todo el ámbito del
método.
A veces, quiere que el método devuelva más que un solo valor. A partir de C# 7.0, puede hacer esto fácilmente
mediante tipos de tupla y literales de tupla. El tipo de tupla define los tipos de datos de los elementos de la tupla.
Los literales de tupla proporcionan los valores reales de la tupla devuelta. En el ejemplo siguiente,
(string, string, string, int) define el tipo de tupla que devuelve el método GetPersonalInfo . La expresión
(per.FirstName, per.MiddleName, per.LastName, per.Age) es el literal de tupla; el método devuelve el nombre, los
apellidos y la edad de un objeto PersonInfo .
public (string, string, string, int) GetPersonalInfo(string id)
{
PersonInfo per = PersonInfo.RetrieveInfoById(id);
return (per.FirstName, per.MiddleName, per.LastName, per.Age);
}
Luego, el autor de la llamada puede usar la tupla devuelta con código como el siguiente:
También se pueden asignar nombres a los elementos de tupla en la definición de tipo de tupla. En el ejemplo
siguiente se muestra una versión alternativa del método GetPersonalInfo que usa elementos con nombre:
public (string FName, string MName, string LName, int Age) GetPersonalInfo(string id)
{
PersonInfo per = PersonInfo.RetrieveInfoById(id);
return (per.FirstName, per.MiddleName, per.LastName, per.Age);
}
Si a un método se pasa una matriz como argumento y modifica el valor de elementos individuales, no es
necesario que el método devuelva la matriz, aunque puede que se prefiera hacerlo a efectos del buen estilo o el
flujo funcional de valores. Esto se debe a que C# pasa todos los tipos de referencia por valor, y el valor de una
referencia a la matriz es el puntero a la matriz. En el ejemplo siguiente, los cambios al contenido de la matriz
values que se realizan en el método DoubleValues los puede observar cualquier código que tenga una
referencia a la matriz.
using System;
Métodos de extensión
Normalmente, hay dos maneras de agregar un método a un tipo existente:
Modificar el código fuente del tipo. No puede hacerlo si no es propietario del código fuente del tipo. Y esto
supone un cambio sustancial si también agrega los campos de datos privados para admitir el método.
Definir el nuevo método en una clase derivada. No se puede agregar un método de este modo con herencia
para otros tipos, como estructuras y enumeraciones. Tampoco se puede usar para agregar un método a una
clase sealed.
Los métodos de extensión permiten agregar un método a un tipo existente sin modificar el propio tipo o
implementar el nuevo método en un tipo heredado. El método de extensión tampoco tiene que residir en el
mismo ensamblado que el tipo que extiende. Llame a un método de extensión como si fuera miembro de un
tipo definido.
Para obtener más información, vea Métodos de extensión.
Métodos asincrónicos
Mediante la característica asincrónica, puede invocar métodos asincrónicos sin usar definiciones de llamada
explícitas ni dividir manualmente el código en varios métodos o expresiones lambda.
Si marca un método con el modificador async, puede usar el operador await en el método. Cuando el control
llega a una expresión await en el método asincrónico, el control se devuelve al autor de la llamada si la tarea
en espera no se ha completado y se suspende el progreso del método con la palabra clave await hasta que
dicha tarea se complete. Cuando se completa la tarea, la ejecución puede reanudarse en el método.
NOTE
Un método asincrónico vuelve al autor de la llamada cuando encuentra el primer objeto esperado que aún no se ha
completado o cuando llega al final del método asincrónico, lo que ocurra primero.
class Program
{
static Task Main() => DoSomethingAsync();
Console.WriteLine($"Result: {result}");
}
Un método asincrónico no puede declarar ningún parámetro in, ref o out, pero puede llamar a los métodos que
tienen estos parámetros.
Para obtener más información sobre los métodos asincrónicos, consulte los artículos Programación asincrónica
con async y await y Tipos de valor devueltos asincrónicos.
public Point Move(int dx, int dy) => new Point(x + dx, y + dy);
public void Print() => Console.WriteLine(First + " " + Last);
// Works with operators, properties, and indexers too.
public static Complex operator +(Complex a, Complex b) => a.Add(b);
public string Name => First + " " + Last;
public Customer this[long id] => store.LookupCustomer(id);
Si el método devuelve void o se trata de un método asincrónico, el cuerpo del método debe ser una expresión
de instrucción (igual que con las expresiones lambda). Para propiedades e indexadores, deben ser de solo
lectura, y no se usa la palabra clave de descriptor de acceso get .
Iterators
Un iterador realiza una iteración personalizada en una colección, como una lista o matriz. Un iterador utiliza la
instrucción yield return para devolver cada elemento de uno en uno. Cuando se llega a una instrucción
yield return , se recuerda la ubicación actual para que el autor de la llamada pueda solicitar el siguiente
elemento en la secuencia.
El tipo de valor devuelto de un iterador puede ser IEnumerable, IEnumerable<T>, IEnumerator o
IEnumerator<T>.
Para obtener más información, consulta Iteradores.
Consulte también
Modificadores de acceso
Clases estáticas y sus miembros
Herencia
Clases y miembros de clase abstractos y sellados
params
out
ref
in
Pasar parámetros
Propiedades
16/09/2021 • 10 minutes to read
Las propiedades son ciudadanos de primera clase en C#. El lenguaje define la sintaxis que permite a los
desarrolladores escribir código que exprese con precisión su intención de diseño.
Las propiedades se comportan como campos cuando se obtiene acceso a ellas. Pero, a diferencia de los campos,
las propiedades se implementan con descriptores de acceso que definen las instrucciones que se ejecutan
cuando se tiene acceso a una propiedad o se asigna.
Una definición de propiedad contiene las declaraciones para un descriptor de acceso get y set que recupera y
asigna el valor de esa propiedad:
La inicialización específica es más útil en las propiedades de solo lectura, como verá posteriormente en este
artículo.
También puede definir su propio almacenamiento, como se muestra a continuación:
public class Person
{
public string FirstName
{
get { return firstName; }
set { firstName = value; }
}
private string firstName;
// remaining implementation removed from listing
}
Cuando una implementación de propiedad es una expresión única, puede usar miembros con forma de
expresión para el captador o establecedor:
Escenarios
Los ejemplos anteriores mostraron uno de los casos más simples de definición de propiedad: una propiedad de
lectura y escritura sin validación. Al escribir el código que quiere en los descriptores de acceso get y set ,
puede crear muchos escenarios diferentes.
Validación
Puede escribir código en el descriptor de acceso set para asegurarse de que los valores representados por una
propiedad siempre son válidos. Por ejemplo, suponga que una regla para la clase Person es que el nombre no
puede estar en blanco ni tener espacios en blanco. Se escribiría de esta forma:
public class Person
{
public string FirstName
{
get => firstName;
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("First name must not be blank");
firstName = value;
}
}
private string firstName;
// remaining implementation removed from listing
}
El ejemplo anterior se puede simplificar usando una expresión throw como parte de la validación del
establecedor de propiedad:
En el ejemplo anterior, se aplica la regla de que el nombre no debe estar en blanco ni tener espacios en blanco.
Si un desarrollador escribe:
hero.FirstName = "";
Esa asignación produce una excepción ArgumentException . Dado que un descriptor de acceso set de propiedad
debe tener un tipo de valor devuelto void, los errores se notifican en el descriptor de acceso set iniciando una
excepción.
Se puede extender esta misma sintaxis para todo lo que se necesite en el escenario. Se pueden comprobar las
relaciones entre las diferentes propiedades o validar con respecto a cualquier condición externa. Todas las
instrucciones de C# válidas son válidas en un descriptor de acceso de propiedad.
Solo lectura
Hasta ahora, todas las definiciones de propiedad que se vieron son propiedades de lectura y escritura con
descriptores de acceso públicos. No es la única accesibilidad válida para las propiedades. Se pueden crear
propiedades de solo lectura, o proporcionar accesibilidad diferente a los descriptores de acceso set y get.
Suponga que su clase Person solo debe habilitar el cambio del valor de la propiedad FirstName desde otros
métodos de esa clase. Podría asignar al descriptor de acceso set la accesibilidad private en lugar de public :
También es válido colocar el modificador más restrictivo en el descriptor de acceso get . Por ejemplo, se podría
tener una propiedad public , pero restringir el descriptor de acceso get a private . Ese escenario raramente
se aplica en la práctica.
También puede restringir las modificaciones de una propiedad, de manera que solo pueda establecerse en un
constructor o en un inicializador de propiedades. Puede modificar la clase Person de la manera siguiente:
Esta característica se usa normalmente para inicializar colecciones que están expuestas como propiedades de
solo lectura:
Propiedades calculadas
Una propiedad no tiene por qué devolver únicamente el valor de un campo de miembro. Se pueden crear
propiedades que devuelvan un valor calculado. Vamos a ampliar el objeto Person para que devuelva el nombre
completo, que se calcula mediante la concatenación del nombre y el apellido:
En el ejemplo anterior se usa la característica de interpolación de cadenas para crear la cadena con formato para
el nombre completo.
También se pueden usar un miembro con forma de expresión, que proporciona una manera más concisa de
crear la propiedad FullName calculada:
public class Person
{
public string FirstName { get; set; }
Los miembros con forma de expresión usan la sintaxis de expresión lambda para definir métodos que contienen
una única expresión. En este caso, esa expresión devuelve el nombre completo para el objeto person.
Propiedades de evaluación en caché
Se puede combinar el concepto de una propiedad calculada con almacenamiento de información y crear una
propiedad de evaluación en caché. Por ejemplo, se podría actualizar la propiedad FullName para que la cadena
de formato solo apareciera la primera vez que se obtuvo acceso a ella:
Pero el código anterior contiene un error. Si el código actualiza el valor de la propiedad FirstName o LastName ,
el campo evaluado previamente fullName no es válido. Hay que modificar los descriptores de acceso set de la
propiedad FirstName y LastName para que el campo fullName se calcule de nuevo:
public class Person
{
private string firstName;
public string FirstName
{
get => firstName;
set
{
firstName = value;
fullName = null;
}
}
Esta versión final da como resultado la propiedad FullName solo cuando sea necesario. Si la versión calculada
previamente es válida, es la que se usa. Si otro cambio de estado invalida la versión calculada previamente, se
vuelve a calcular. No es necesario que los desarrolladores que usan esta clase conozcan los detalles de la
implementación. Ninguno de estos cambios internos afectan al uso del objeto Person. Es el motivo principal
para usar propiedades para exponer los miembros de datos de un objeto.
Asociar atributos a propiedades implementadas automáticamente
A partir de C# 7.3, los atributos de campo se pueden conectar al campo de respaldo generado por el compilador
en las propiedades implementadas automáticamente. Por ejemplo, pensemos en una revisión de la clase
Person que agrega una propiedad Id de entero único. Escribe la propiedad Id usando una propiedad
implementada automáticamente, pero el diseño no requiere que la propiedad Id se conserve.
NonSerializedAttribute solo se puede asociar a campos, no a propiedades. NonSerializedAttribute se puede
asociar al campo de respaldo de la propiedad Id usando el especificador field: en el atributo, como se
muestra en el siguiente ejemplo:
public class Person
{
public string FirstName { get; set; }
[field:NonSerialized]
public int Id { get; set; }
Esta técnica funciona con cualquier atributo que se asocie al campo de respaldo en la propiedad implementada
automáticamente.
Implementar INotifyPropertyChanged
Un último escenario donde se necesita escribir código en un descriptor de acceso de propiedad es para admitir
la interfaz INotifyPropertyChanged que se usa para notificar a los clientes de enlace de datos el cambio de un
valor. Cuando se cambia el valor de una propiedad, el objeto genera el evento
INotifyPropertyChanged.PropertyChanged para indicar el cambio. A su vez, las bibliotecas de enlace de datos
actualizan los elementos de visualización en función de ese cambio. El código siguiente muestra cómo se
implementaría INotifyPropertyChanged para la propiedad FirstName de esta clase person.
El operador ?. se denomina operador condicional NULL. Comprueba si existe una referencia nula antes de
evaluar el lado derecho del operador. El resultado final es que si no hay ningún suscriptor para el evento
PropertyChanged , no se ejecuta el código para generar el evento. En ese caso, se producirá una
NullReferenceException sin esta comprobación. Para obtener más información, vea events . En este ejemplo
también se usa el nuevo operador nameof para convertir el símbolo de nombre de propiedad en su
representación de texto. Con nameof se pueden reducir los errores en los que no se escribió correctamente el
nombre de la propiedad.
De nuevo, la implementación de INotifyPropertyChanged es un ejemplo de un caso en el que se puede escribir
código en los descriptores de acceso para admitir los escenarios que se necesitan.
Resumen
Las propiedades son una forma de campos inteligentes en una clase o un objeto. Desde fuera del objeto,
parecen campos en el objeto. Pero las propiedades pueden implementarse mediante la paleta completa de
funcionalidad de C#. Se puede proporcionar validación, tipos diferentes de accesibilidad, evaluación diferida o
los requisitos que se necesiten para cada escenario.
Indizadores
16/09/2021 • 9 minutes to read
Los indizadores son similares a las propiedades. Muchas veces, los indizadores se basan en las mismas
características del lenguaje que las propiedades. Los indizadores permiten las propiedades indizadas:
propiedades a las que se hace referencia mediante uno o más argumentos. Estos argumentos proporcionan un
índice en alguna colección de valores.
Los indizadores se declaran con la palabra clave this como nombre de la propiedad y declarando los
argumentos entre corchetes. Esta declaración coincidiría con el uso que se muestra en el párrafo anterior:
En este ejemplo inicial puede ver la relación existente entre la sintaxis de las propiedades y los indizadores. Esta
analogía lleva a cabo la mayoría de las reglas de sintaxis de los indizadores. Los indizadores pueden tener
cualquier modificador de acceso válido (público, interno protegido, protegido, interno, privado o privado
protegido). Pueden ser sellados, virtuales o abstractos. Al igual que con las propiedades, puede especificar
distintos modificadores de acceso para los descriptores de acceso get y set en un indizador. También puede
especificar indizadores de solo lectura (omitiendo el descriptor de acceso set) o indizadores de solo escritura
(omitiendo el descriptor de acceso get).
Puede aplicar a los indizadores casi todo lo que aprenda al trabajar con propiedades. La única excepción a esta
regla son las propiedades implementadas automáticamente. El compilador no siempre puede generar el
almacenamiento correcto para un indizador.
La presencia de argumentos para hacer referencia a un elemento en un conjunto de elementos distingue los
indizadores de las propiedades. Puede definir varios indizadores en un tipo, mientras que las listas de
argumentos de cada indizador son únicas. Vamos a explorar escenarios diferentes en los que puede usar uno o
varios indizadores en una definición de clase.
Escenarios
Tendría que definir indizadores en el tipo si su API modela alguna colección en la que se definen los argumentos
de esa colección. Los indizadores pueden (o no) asignarse directamente a los tipos de colección que forman
parte del marco de trabajo principal de .NET. El tipo puede tener otras responsabilidades, además de tener que
modelar una colección. Los indizadores le permiten proporcionar la API que coincida con la abstracción de su
tipo sin tener que exponer la información interna de cómo se almacenan o se calculan los valores de dicha
abstracción.
Veamos algunos de los escenarios habituales en los que se usan los indizadores. Puede obtener acceso a la
carpeta de ejemplo para indexadores. Para obtener instrucciones de descarga, vea Ejemplos y tutoriales.
Matrices y vectores
Uno de los escenarios más comunes para crear indizadores es cuando el tipo modela una matriz o un vector.
Puede crear un indizador para modelar una lista ordenada de datos.
La ventaja de crear su propio indizador es que puede definir el almacenamiento de esa colección para satisfacer
sus necesidades. Piense en un escenario en el que el tipo modela datos históricos que tienen un tamaño
demasiado grande para poder cargarlos en la memoria de una vez. Debe cargar y descargar secciones de la
colección en función del uso. En el ejemplo siguiente se modela este comportamiento. Informa sobre el número
de puntos de datos existente, crea páginas para incluir secciones de los datos a petición y quita páginas de la
memoria para dejar espacio para las páginas necesarias para las solicitudes más recientes.
page[index] = value;
}
}
Puede seguir esta expresión de diseño para modelar cualquier tipo de colección cuando haya motivos de peso
para no cargar todo el conjunto de datos en una colección en memoria. Observe que la clase Page es una clase
anidada privada que no forma parte de la interfaz pública. Estos datos se ocultan a los usuarios de esta clase.
Diccionarios
Otro escenario habitual es cuando necesita modelar un diccionario o una asignación. En este escenario, el tipo
almacena valores en función de la clave (normalmente claves de texto). En este ejemplo se crea un diccionario
que asigna argumentos de la línea de comandos a expresiones lambda que administran estas opciones. En el
ejemplo siguiente se muestran dos clases: una clase ArgsActions , que asigna una opción de la línea de
comandos a un delegado Action ; y ArgsProcessor , que usa ArgsActions para ejecutar cada Action cuando
encuentra esa opción.
}
public class ArgsActions
{
readonly private Dictionary<string, Action> argsActions = new Dictionary<string, Action>();
En este ejemplo, la colección ArgsAction está estrechamente relacionada con la colección subyacente. get
determina si se ha configurado una opción determinada. Si es así, devuelve la Action asociada a esa opción. Si
no, devuelve una Action que no hace nada. El descriptor de acceso público no incluye ningún descriptor de
acceso set , sino el diseño que usa un método público para establecer opciones.
Asignaciones multidimensionales
Puede crear indizadores que usen varios argumentos. Además, estos argumentos no se restringen para que
sean del mismo tipo. Veamos dos ejemplos.
En el primer ejemplo se muestra una clase que genera valores para el conjunto Mandelbrot. Para obtener más
información sobre las matemáticas subyacentes en el conjunto, lea este artículo. El indizador usa dos valores
double para definir un punto en el plano X, Y. El descriptor de acceso get calcula el número de iteraciones
existente hasta que se determina que un punto no está en el conjunto. Si se alcanza el número máximo de
iteraciones, el punto está en el conjunto y se devuelve el valor maxIterations de la clase (las imágenes generadas
por PC popularizadas para el conjunto Mandelbrot definen colores para el número de iteraciones que son
necesarias para determinar que un punto en concreto está fuera del conjunto).
El conjunto Mandelbrot define valores en cada coordenada (x o y) para los valores numéricos reales. Con esto se
define un diccionario que puede contener un número infinito de valores. Por lo tanto, no hay ningún
almacenamiento detrás del conjunto. En su lugar, esta clase calcula el valor de cada punto cuando el código
llama al descriptor de acceso get , por lo que no se usa ningún almacenamiento subyacente.
Vamos a examinar un último uso de los indizadores, en el que el indizador toma varios argumentos de distintos
tipos. Imagínese un programa que administra datos históricos de temperaturas. Este indizador usa una ciudad y
una fecha para establecer u obtener las temperaturas máximas y mínimas de ese lugar:
using DateMeasurements =
System.Collections.Generic.Dictionary<System.DateTime, IndexersSamples.Common.Measurements>;
using CityDataMeasurements =
System.Collections.Generic.Dictionary<string, System.Collections.Generic.Dictionary<System.DateTime,
IndexersSamples.Common.Measurements>>;
Este ejemplo crea un indizador que asigna los datos meteorológicos en dos argumentos diferentes: una ciudad
(representada por string ) y una fecha (representada por DateTime ). El almacenamiento interno usa dos clases
Dictionary para representar el diccionario bidimensional. La API pública ya no representa el almacenamiento
subyacente. En su lugar, las características del lenguaje de los indizadores le permiten crear una interfaz pública
que representa la abstracción, aunque el almacenamiento subyacente debe usar distintos tipos de colección
básica.
Hay dos partes de este código que pueden resultar desconocidas para algunos desarrolladores, Estas dos
directivas using :
Estas instrucciones crean un alias para un tipo genérico construido y permiten que el código use después los
nombres DateMeasurements y CityDateMeasurements (más descriptivos) en vez de la construcción genérica de
Dictionary<DateTime, Measurements> y Dictionary<string, Dictionary<DateTime, Measurements> > . Esta
construcción requiere el uso de los nombres completos de tipo en el lado derecho del signo = .
La segunda técnica consiste en quitar las partes de tiempo de cualquier objeto DateTime usado para indizarse
en las colecciones. .NET no incluye un tipo de solo fecha. Los desarrolladores usan el tipo DateTime , aunque
emplean la propiedad Date para asegurarse de que cualquier objeto DateTime de ese día sea igual.
Resumen
Debe crear indizadores siempre que tenga un elemento de propiedad en la clase, en la que dicha propiedad no
representa un valor único, sino una serie de valores donde cada elemento se identifica mediante un conjunto de
argumentos. Estos argumentos únicamente pueden identificar el elemento al que se debe hacer referencia en la
colección. Los indizadores amplían el concepto de las propiedades, en las que un miembro se trata como un
elemento de datos desde fuera de la clase, pero como un método desde dentro. Los indizadores permiten que
los argumentos busquen un solo elemento en una propiedad que representa un conjunto de elementos.
Iterators
16/09/2021 • 6 minutes to read
Prácticamente todos los programas que escriba tendrán alguna necesidad de recorrer en iteración una
colección. Va a escribir código que examine cada elemento de una colección.
También va a crear métodos de iterador, que son los métodos que genera un iterador para los elementos de esa
clase. Un iterador es un objeto que atraviesa un contenedor, especialmente las listas. Los iteradores se pueden
usar para:
Realizar una acción en cada elemento de una colección.
Enumerar una colección personalizada.
Extender LINQ u otras bibliotecas.
Crear una canalización de datos en la que los datos fluyan de forma eficaz mediante métodos de iterador.
El lenguaje C# proporciona características para generar y consumir secuencias. Estas secuencias se pueden
generar y consumir de forma sincrónica o asincrónica. Este artículo proporciona información general sobre esas
características.
That's all. (Esto es todo) Para recorrer en iteración todo el contenido de una colección, la instrucción foreach es
todo lo que necesita. Pero la instrucción foreach no es mágica. Depende de dos interfaces genéricas definidas
en la biblioteca de .NET Core para generar el código necesario para recorrer en iteración una colección:
IEnumerable<T> e IEnumerator<T> . Este mecanismo se explica con más detalle a continuación.
Ambas interfaces tienen también homólogas no genéricas: IEnumerable e IEnumerator . Para el código moderno
se prefieren las versiones genéricas.
Cuando se genera una secuencia de forma asincrónica, puede usar la instrucción await foreach para consumir
la secuencia de forma asincrónica:
El código anterior muestra instrucciones distintivas yield return para resaltar el hecho de que se pueden usar
varias instrucciones discretas yield return en un método de iterador. Puede usar (y hágalo a menudo) otras
construcciones de lenguaje para simplificar el código de un método de iterador. La definición del método
siguiente genera la misma secuencia de números:
No tiene que elegir entre una y otra. Puede tener tantas instrucciones yield return como sea necesario para
satisfacer las necesidades del método:
index = 100;
while (index < 110)
yield return index++;
}
Todos estos ejemplos anteriores tendrían un homólogo asincrónico. En cada caso, reemplazaría el tipo de valor
devuelto de IEnumerable<T> por un elemento IAsyncEnumerable<T> . Por ejemplo, el ejemplo anterior tendría la
siguiente versión asincrónica:
public async IAsyncEnumerable<int> GetSetsOfNumbersAsync()
{
int index = 0;
while (index < 10)
yield return index++;
await Task.Delay(500);
await Task.Delay(500);
index = 100;
while (index < 110)
yield return index++;
}
Esta es la sintaxis de los iteradores sincrónicos y asincrónicos. Veamos un ejemplo del mundo real. Imagine que
se encuentra en un proyecto de IoT y los sensores del dispositivo generan un flujo de datos muy grande. Para
hacerse una idea de los datos, podría escribir un método que tomara muestras de cada enésimo elemento de
datos. Este pequeño método de iterador lo hace:
Si la lectura desde el dispositivo IoT genera una secuencia asincrónica, modificaría el método como se muestra
en el método siguiente:
Hay una restricción importante en los métodos de iterador: no puede tener una instrucción return y una
instrucción yield return en el mismo método. El código siguiente no se compilará:
public IEnumerable<int> GetSingleDigitNumbers()
{
int index = 0;
while (index < 10)
yield return index++;
Normalmente esta restricción no supone un problema. Tiene la opción de usar yield return en todo el método
o de separar el método original en varios métodos, unos con return y otros con yield return .
Puede modificar el último método ligeramente para usar yield return en todas partes:
var items = new int[] {100, 101, 102, 103, 104, 105, 106, 107, 108, 109 };
foreach (var item in items)
yield return item;
}
A veces, la respuesta correcta es dividir un método de iterador en dos métodos distintos. Uno que use return y
un segundo que use yield return . Imagine una situación en la que quiera devolver una colección vacía, o los
cinco primeros números impares, basándose en un argumento booleano. Eso se podría escribir como estos dos
métodos:
Observe los métodos anteriores. El primero usa la instrucción estándar return para devolver una colección
vacía o el iterador creado por el segundo método. El segundo método usa la instrucción yield return para
crear la secuencia solicitada.
Profundización en foreach
La instrucción se expande en un elemento estándar que usa las interfaces IEnumerable<T> e
foreach
IEnumerator<T> para recorrer en iteración todos los elementos de una colección. También minimiza los errores
cometidos por los desarrolladores al no administrar correctamente los recursos.
El compilador traduce el bucle foreach que se muestra en el primer ejemplo en algo similar a esta
construcción:
El código exacto generado por el compilador es más complicado y controla las situaciones en las que el objeto
devuelto por GetEnumerator() implementa la interfaz IDisposable . La expansión completa genera código más
parecido al siguiente:
{
var enumerator = collection.GetEnumerator();
try
{
while (enumerator.MoveNext())
{
var item = enumerator.Current;
Console.WriteLine(item.ToString());
}
}
finally
{
// dispose of enumerator.
}
}
{
var enumerator = collection.GetEnumerator();
try
{
while (await enumerator.MoveNext())
{
var item = enumerator.Current;
Console.WriteLine(item.ToString());
}
}
finally
{
// dispose of async enumerator.
}
}
La manera en que el enumerador se elimina depende de las características del tipo de enumerator . En el caso
sincrónico general, la cláusula finally se expande a:
finally
{
(enumerator as IDisposable)?.Dispose();
}
finally
{
if (enumerator is IAsyncDisposable asyncDisposable)
await asyncDisposable.DisposeAsync();
}
Sin embargo, si el tipo de enumerator es un tipo sellado y no hay conversión implícita del tipo de enumerator a
IDisposable o IAsyncDisposable , la cláusula finally se expande en un bloque vacío:
finally
{
}
Si hay una conversión implícita del tipo de enumerator a IDisposable ,y enumerator es un tipo de valor que no
acepta valores Null, la cláusula finally se expande en:
finally
{
((IDisposable)enumerator).Dispose();
}
Afortunadamente, no es necesario recordar todos estos detalles. La instrucción foreach controla todos esos
matices. El compilador generará el código correcto para cualquiera de estas construcciones.
Introducción a los delegados
16/09/2021 • 2 minutes to read
Los delegados proporcionan un mecanismo de enlace en tiempo de ejecución en .NET. Un enlace en tiempo de
ejecución significa que se crea un algoritmo en el que el llamador también proporciona al menos un método
que implementa parte del algoritmo.
Por ejemplo, considere la ordenación de una lista de estrellas en una aplicación de astronomía. Puede decidir
ordenar las estrellas por su distancia con respecto a la Tierra, por la magnitud de la estrella o por su brillo
percibido.
En todos estos casos, el método Sort() hace básicamente lo mismo: organiza los elementos en la lista en función
de una comparación. El código que compara dos estrellas es diferente para cada uno de los criterios de
ordenación.
Este tipo de soluciones se ha usado en software durante aproximadamente medio siglo. El concepto de
delegado del lenguaje C# proporciona compatibilidad con el lenguaje de primera clase y seguridad de tipos en
torno a este concepto.
Como verá más adelante en esta serie, el código de C# que escriba para algoritmos como este posee seguridad
de tipos y usa las reglas del lenguaje y el compilador para asegurarse de que los tipos coincidan con los
argumentos y los tipos devueltos.
Se han agregado punteros de función a C# 9 para escenarios similares, donde se necesita más control sobre la
convención de llamadas. El código asociado a un delegado se invoca mediante un método virtual agregado a un
tipo de delegado. Mediante los punteros de función puede especificar otras convenciones.
Anterior
En este artículo se tratan las clases de .NET que admiten delegados y sobre cómo se asignan a la palabra clave
delegate .
El compilador genera una clase derivada de System.Delegate que coincide con la firma usada (en este caso, un
método que devuelve un entero y tiene dos argumentos). El tipo de ese delegado es Comparison . El tipo de
delegado Comparison es un tipo genérico. Aquí puede obtener más información sobre los genéricos.
Observe que puede parecer que la sintaxis declara una variable, pero en realidad declara un tipo. Puede definir
tipos de delegado dentro de clases, directamente dentro de espacios de nombres o incluso en el espacio de
nombres global.
NOTE
No se recomienda declarar tipos de delegado (u otros tipos) directamente en el espacio de nombres global.
El compilador también genera controladores de adición y eliminación para este nuevo tipo, de modo que los
clientes de esta clase puedan agregar y quitar métodos de la lista de invocación de una instancia. El compilador
forzará que la firma del método que se agrega o se quita coincida con la firma usada al declarar el método.
El fragmento de código anterior declara una variable de miembro dentro de una clase. También puede declarar
variables de delegado que sean variables locales o argumentos para los métodos.
Invocación de delegados
Para invocar los métodos que se encuentran en la lista de invocación de un delegado, llame a dicho delegado.
Dentro del método Sort() , el código llamará al método de comparación para determinar en qué orden
colocará los objetos:
En la línea anterior, el código invoca al método asociado al delegado. La variable se trata como un nombre de
método y se invoca mediante la sintaxis de llamada de método normal.
Esta línea de código realiza una suposición arriesgada, ya que no hay ninguna garantía de que se haya agregado
un destino al delegado. Si no se ha asociado ningún destino, la línea anterior haría que se produjese una
NullReferenceException . Las expresiones que se usan para resolver este problema son más complicadas que
una simple comprobación de null y se tratan más adelante en esta serie.
El método se ha declarado como un método privado. Esto es correcto, ya que tal vez no le interese que este
método forme parte de la interfaz pública. Aun así, puede usarse como método de comparación cuando se
asocia a un delegado. El código de llamada tendrá este método asociado a la lista de destino del objeto de
delegado y puede tener acceso a él a través de ese delegado.
Para crear esta relación, pase ese método al método List.Sort() :
phrases.Sort(CompareLength);
Observe que se usa el nombre del método sin paréntesis. Al usar el método como un argumento, le indica al
compilador que convierta la referencia del método en una referencia que se puede usar como un destino de
invocación del delegado y que asocie ese método como un destino de invocación.
También podría haber declarado de forma explícita una variable de tipo Comparison<string> y realizado una
asignación:
En los casos en los que el método que se usa como destino del delegado es un método pequeño, es habitual
usar la sintaxis de expresión lambda para realizar la asignación:
El uso de expresiones lambda para destinos de delegados se explica con más detalle en una sección posterior.
En el ejemplo de Sort() se suele asociar un método de destino único al delegado, pero los objetos delegados
admiten listas de invocación que tienen varios métodos de destino asociados a un objeto delegado.
Anterior
En el artículo anterior pudo ver que con la palabra clave delegate se crean tipos de delegados concretos.
La clase abstracta Delegate proporciona la infraestructura para el acoplamiento flexible y la invocación. Los tipos
de delegado concretos se hacen mucho más útiles al adoptar y aplicar la seguridad de tipos para los métodos
agregados a la lista de invocación de un objeto de delegado. Cuando se usa palabra clave delegate y se define
un tipo de delegado concreto, el compilador genera esos métodos.
En la práctica, esto daría lugar a la creación de nuevos tipos de delegado siempre que necesitara otra firma de
método. Este trabajo podría resultar tedioso pasado un tiempo. Cada nueva característica exige nuevos tipos de
delegado.
Afortunadamente, esto no es necesario. .NET Core Framework contiene varios tipos que puede volver a usar
siempre que necesite tipos de delegado. Son definiciones genéricas, por lo que puede declarar
personalizaciones cuando necesite nuevas declaraciones de método.
El primero de estos tipos es el tipo Action y distintas variaciones:
El modificador out del argumento de tipo genérico result se trata en el artículo sobre la covarianza.
Hay variaciones del delegado Func con hasta 16 argumentos de entrada, como
Func<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,T15,T16,TResult>. Por convención, el tipo del resultado
siempre es el último parámetro de tipo de todas las declaraciones Func .
Use uno de los tipos Func para cualquier tipo de delegado que devuelva un valor.
También hay un tipo Predicate<T> especializado para un delegado que devuelva una prueba sobre un valor
único:
public delegate bool Predicate<in T>(T obj);
Es posible que observe que para cualquier tipo Predicate existe un tipo Func estructuralmente equivalente,
por ejemplo:
Podría llegar a pensar que estos dos tipos son equivalentes, pero no lo son. Estas dos variables no se pueden
usar indistintamente. A una variable de un tipo no se le puede asignar el otro tipo. El sistema de tipos de C# usa
los nombres de los tipos definidos, no la estructura.
Todas estas definiciones de tipos de delegado de la biblioteca de .NET Core deberían significar que no es
necesario definir ningún tipo de delegado nuevo para cualquier característica nueva creada que exija delegados.
Estas definiciones genéricas deberían proporcionar todos los tipos de delegado necesarios para la mayoría de
las situaciones. Puede simplemente crear instancias de uno de estos tipos con los parámetros de tipo necesarios.
En el caso de los algoritmos que se pueden convertir en genéricos, estos delegados se pueden usar como tipos
genéricos.
Esto debería ahorrar tiempo y minimizar el número de nuevos tipos que es necesario crear para poder trabajar
con delegados.
En el siguiente artículo se verán varios patrones comunes para trabajar con delegados en la práctica.
Siguiente
Patrones comunes para delegados
16/09/2021 • 8 minutes to read
Anterior
Los delegados proporcionan un mecanismo que permite que los diseños de software supongan un
acoplamiento mínimo entre los componentes.
Un ejemplo excelente de este tipo de diseño es LINQ. El patrón de expresión de consulta LINQ se basa en los
delegados para todas sus características. Considere este ejemplo sencillo:
Se filtra la secuencia solo de los números que son inferiores al valor 10. El método Where usa un delegado que
determina qué elementos de una secuencia pasan el filtro. Cuando crea una consulta LINQ, proporciona la
implementación del delegado para este fin específico.
El prototipo para el método Where es:
Este ejemplo se repite con todos los métodos que forman parte de LINQ. Todos se basan en delegados para el
código que administra la consulta específica. Este modelo de diseño de API es eficaz para obtener información y
comprender.
En este ejemplo sencillo se ilustra cómo los delegados necesitan muy poco acoplamiento entre componentes.
No necesita crear una clase que derive de una clase base determinada. No necesita implementar una interfaz
específica. El único requisito consiste en proporcionar la implementación de un método que sea fundamental
para la tarea que nos ocupa.
La clase estática anterior es lo más sencillo que puede funcionar. Necesitamos escribir solo la implementación
para el método que escribe mensajes en la consola:
Por último, necesita conectar el delegado asociándolo al delegado WriteMessage que se declara en el
registrador:
Logger.WriteMessage += LoggingMethods.LogToConsole;
Procedimientos
Hasta ahora nuestro ejemplo es bastante sencillo, pero sigue mostrando algunas instrucciones importantes para
los diseños que involucran a los delegados.
Con los tipos de delegado definidos en el marco de trabajo principal es más sencillo para los usuarios trabajar
con los delegados. No necesita definir tipos nuevos, y los desarrolladores que usen su biblioteca no necesitan
aprender nuevos tipos de delegado especializados.
Las interfaces que se han usado son tan mínimas y flexibles como es posible: para crear un registrador de salida
nuevo, debe crear un método. Ese método puede ser un método estático o un método de instancia. Puede tener
cualquier acceso.
Formato de salida
Vamos a hacer esta primera versión un poco más sólida y, después, empezaremos a crear otros mecanismos de
registro.
Después, vamos a agregar algunos argumentos al método LogMessage() de manera que su clase de registro
cree más mensajes estructurados:
public enum Severity
{
Verbose,
Trace,
Information,
Warning,
Error,
Critical
}
A continuación, vamos a usar ese argumento Severity para filtrar los mensajes que se envían a la salida del
registro.
Procedimientos
Ha agregado características nuevas a la infraestructura de registro. Como el componente del registrador se
acopla débilmente a cualquier mecanismo de salida, estas características nuevas pueden agregarse sin afectar a
ningún código que implementa el delegado del registrador.
A medida que siga creando esto, verá más ejemplos de cómo este acoplamiento débil permite una mayor
flexibilidad en la actualización de las partes del sitio sin que haya cambios en otras ubicaciones. De hecho, en
una aplicación más grande, las clases de salida del registrador pueden estar en un ensamblado diferente, y ni
siquiera necesitan volver a crearse.
Una vez que haya creado esta clase, puede inicializarla y esta asocia su método LogMessage al componente de
registrador:
Estos dos no son mutuamente exclusivos. Puede asociar ambos métodos de registro y generar mensajes en la
consola y en un archivo:
Después, incluso en la misma aplicación, puede quitar uno de los delegados sin ocasionar ningún otro problema
en el sistema:
Logger.WriteMessage -= LoggingMethods.LogToConsole;
Procedimientos
Ahora, ha agregado un segundo controlador de salida para el subsistema de registro. Este necesita un poco más
de infraestructura para admitir correctamente el sistema de archivos. El delegado es un método de instancia.
También es un método privado. No existe ninguna necesidad de una mayor accesibilidad porque la
infraestructura de delegado puede conectarse a los delegados.
En segundo lugar, el diseño basado en delegados permite varios métodos de salida sin ningún código adicional.
No necesita crear ninguna infraestructura adicional para admitir varios métodos de salida. Simplemente se
convierten en otro método en la lista de invocación.
Preste una atención especial al código del método de salida de registro de archivo. Se codifica para garantizar
que no produce ninguna excepción. Aunque esto no siempre es estrictamente necesario, a menudo es un buen
procedimiento. Si cualquiera de los métodos de delegado produce una excepción, los delegados restantes que
se encuentran en la invocación no se invocarán.
Como última observación, el registrador de archivos debe administrar sus recursos abriendo y cerrando el
archivo en cada mensaje de registro. Puede optar por mantener el archivo abierto e implementar IDisposable
para cerrar el archivo cuando termine. Cualquier método tiene sus ventajas e inconvenientes. Ambos crean un
poco más de acoplamiento entre las clases.
Ninguna parte del código de la clase Logger tendrá que actualizarse para admitir cualquiera de los escenarios.
El operador condicional NULL ( ?. ) crea un cortocircuito cuando el operando izquierdo ( WriteMessage en este
caso) es NULL, lo que significa que no se realiza ningún intento para registrar un mensaje.
No encontrará el método Invoke() en la documentación de System.Delegate o System.MulticastDelegate . El
compilador genera un método Invoke con seguridad de tipos para cualquier tipo de delegado declarado. En
este ejemplo, eso significa que Invoke toma un solo argumento string y tiene un tipo de valor devuelto void.
Resumen de procedimientos
Ha observado los comienzos de un componente de registro que puede expandirse con otros sistemas de
escritura y otras características. Al usar delegados en el diseño, estos distintos componentes están acoplados
débilmente. Esto ofrece varias ventajas. Es sencillo crear mecanismos de salida nuevos y asociarlos al sistema.
Estos otros mecanismos solo necesitan un método: el método que escribe el mensaje de registro. Es un diseño
que es resistente cuando se agregan características nuevas. El contrato que se necesita para cualquier sistema
de escritura es implementar un método. Ese método puede ser un método estático o de instancia. Puede ser
público, privado o de cualquier otro acceso legal.
La clase de registrador puede realizar cualquier número de cambios o mejoras sin producir cambios
importantes. Como cualquier clase, no puede modificar la API pública sin el riesgo de que se produzcan cambios
importantes. Pero, como el acoplamiento entre el registrador y cualquier motor de salida se realiza solo
mediante el delegado, ningún otro tipo (como interfaces o clases base) está involucrado. El acoplamiento es lo
más pequeño posible.
Siguiente
Introducción a los eventos
16/09/2021 • 3 minutes to read
Anterior
Los eventos son, como los delegados, un mecanismo de enlace en tiempo de ejecución. De hecho, los eventos se
crean con compatibilidad de lenguaje para los delegados.
Los eventos son una manera para que un objeto difunda (a todos los componentes interesados del sistema) que
algo ha sucedido. Cualquier otro componente puede suscribirse al evento, y recibir una notificación cuando se
genere uno.
Probablemente ha usado eventos en alguna programación. Muchos sistemas gráficos tienen un modelo de
eventos para notificar la interacción del usuario. Estos eventos notificarán movimiento del mouse, pulsaciones
de botón e interacciones similares. Ese es uno de los más comunes, pero realmente no es el único escenario
donde se usan eventos.
Puede definir eventos que deben generarse para las clases. Una consideración importante a la hora de trabajar
con eventos es que puede que no haya ningún objeto registrado para un evento determinado. Debe escribir el
código de manera que no genere eventos cuando no esté configurado ningún agente de escucha.
La suscripción a un evento también crea un acoplamiento entre dos objetos (el origen del evento y el receptor
del evento). Necesita asegurarse de que el receptor del evento cancela la suscripción del origen del evento
cuando ya no está interesado en eventos.
El tipo del evento ( EventHandler<FileListArgs> en este ejemplo) debe ser un tipo de delegado. Existen varias
convenciones que debe seguir al declarar un evento. Normalmente, el tipo de delegado de eventos tiene un
valor devuelto void. Las declaraciones de eventos deben ser un verbo o una frase verbal. Use un tiempo verbal
pasado cuando el evento notifique algo que ha ocurrido. Use un tiempo verbal presente (por ejemplo, Closing )
para notificar algo que está a punto de suceder. A menudo, el uso del tiempo presente indica que su clase
admite algún tipo de comportamiento de personalización. Uno de los escenarios más comunes es admitir la
cancelación. Por ejemplo, un evento Closing puede incluir un argumento que indicará si la operación de cierre
debe continuar o no. Otros escenarios pueden permitir que los autores de la llamada modifiquen el
comportamiento actualizando propiedades de los argumentos de eventos. Puede generar un evento para
indicar la siguiente acción propuesta que realizará un algoritmo. El controlador de eventos puede exigir una
acción diferente modificando las propiedades del argumento de eventos.
Cuando quiera generar el evento, llame a los controladores de eventos mediante la sintaxis de invocación del
delegado:
Como se ha tratado en la sección sobre delegados, el operador ?. hace que se garantice más fácilmente que no
intenta generar el evento cuando no existen suscriptores en este.
Se suscribe a un evento con el operador += :
fileLister.Progress += onProgress;
El método de controlador normalmente tiene el prefijo "On" seguido del nombre del evento, como se ha
mostrado anteriormente.
Cancela la suscripción con el operador -= :
fileLister.Progress -= onProgress;
Es importante que declare una variable local para la expresión que representa el controlador de eventos. Eso
garantiza que la cancelación de la suscripción quita el controlador. Si, en su lugar, ha usado el cuerpo de la
expresión lambda, está intentando quitar un controlador que nunca ha estado asociado, lo que no produce
ninguna acción.
En el artículo siguiente, obtendrá más información sobre los modelos de eventos típicos y las diferentes
variaciones de este ejemplo.
Siguiente
Patrón de eventos estándar de .NET
16/09/2021 • 8 minutes to read
Anterior
Los eventos de .NET generalmente siguen unos patrones conocidos. Estandarizar sobre estos patrones significa
que los desarrolladores pueden aprovechar el conocimiento de esos patrones estándar, que se pueden aplicar a
cualquier programa de evento de .NET.
Vamos a analizar los patrones estándar, para que tenga todos los conocimientos necesarios para crear orígenes
de eventos estándar y suscribirse y procesar eventos estándar en el código.
El tipo de valor devuelto es void. Los eventos se basan en delegados y son delegados de multidifusión. Eso
admite varios suscriptores para cualquier origen de eventos. El único valor devuelto de un método no escala a
varios suscriptores de eventos. ¿Qué valor devuelto ve el origen de evento después de generar un evento? Más
adelante en este artículo verá cómo crear protocolos de evento que admiten suscriptores de eventos que
notifican información al origen del evento.
La lista de argumentos contiene dos argumentos: el remitente y los argumentos del evento. El tipo de tiempo de
compilación de sender es System.Object , aunque probablemente conozca un tipo más derivado que siempre
será correcto. Por convención, use object .
Típicamente, el segundo argumento era un tipo que se derivaba de System.EventArgs . (Verá en la siguiente
sección que ya no se aplica esta convención). Si el tipo de evento no necesita ningún argumento adicional, aún
tendrá que proporcionar los dos argumentos. Hay un valor especial, EventArgs.Empty , que debe usarse para
indicar que el evento no contiene ninguna información adicional.
Vamos a crear una clase que enumera los archivos en un directorio, o cualquiera de sus subdirectorios que
siguen un patrón. Este componente genera un evento para cada archivo encontrado que coincida con el modelo.
El uso de un modelo de eventos proporciona algunas ventajas de diseño. Se pueden crear varios agentes de
escucha de eventos que realicen acciones diferentes cuando se encuentre un archivo buscado. La combinación
de los distintos agentes de escucha puede crear algoritmos más sólidos.
Esta es la declaración del argumento de evento inicial para buscar un archivo buscado:
Aunque este tipo parece un tipo pequeño exclusivo para datos, debe seguir la convención y convertirlo en un
tipo de referencia ( class ). Esto significa que el objeto de argumento se pasará por referencia y que todos los
suscriptores verán las actualizaciones de los datos. La primera versión es un objeto inmutable. Es preferible
hacer que las propiedades en el tipo de argumento de evento sean inmutables. De ese modo, un suscriptor no
puede cambiar los valores antes de que los vea otro suscriptor. (Hay excepciones, como verá a continuación).
Después, debemos crear la declaración de evento en la clase FileSearcher. Aprovechando el tipo
EventHandler<T> , no es necesario crear otra definición de tipo más. Simplemente se puede usar una
especialización genérica.
Vamos a rellenar la clase FileSearcher para buscar archivos que coincidan con un patrón y generar el evento
correcto cuando se detecte una coincidencia.
Parece que se está declarando un campo público, lo que podría parecer una práctica orientada a objetos
incorrecta. Quiere proteger el acceso a los datos a través de propiedades o métodos. Aunque esto puede parecer
una mala práctica, el código generado por el compilador crea contenedores para que solo se pueda acceder de
forma segura a los objetos de evento. Las únicas operaciones disponibles en un evento con aspecto de campo
son las de agregar controlador:
fileLister.FileFound += onFileFound;
y quitar controlador:
fileLister.FileFound -= onFileFound;
Tenga en cuenta que hay una variable local para el controlador. Si usó el cuerpo de la expresión lambda, la
eliminación no funcionará correctamente. Sería una instancia diferente del delegado y, en modo silencioso, no
se hace nada.
El código fuera de la clase no puede generar el evento, ni puede realizar otras operaciones.
Devolución de valores desde los suscriptores de eventos
La versión simple funciona correctamente. Vamos a agregar otra característica: la cancelación.
Cuando se genera el evento encontrado, los agentes de escucha deberían ser capaces de detener el
procesamiento, si este archivo es el último que se busca.
Los controladores de eventos no devuelven un valor, por lo que se necesita comunicarlo de otra forma. El patrón
de eventos estándar usa el objeto EventArgs para incluir campos que los suscriptores de eventos pueden usar
para comunicar la cancelación.
Existen dos patrones diferentes que podrían usarse, basándose en la semántica del contrato de cancelación. En
ambos casos, se agrega un campo booleano a EventArguments para el evento del archivo encontrado.
Uno de los patrones permitiría a cualquier suscriptor cancelar la operación. Para este patrón, el nuevo campo se
inicializa en false . Los suscriptores pueden cambiarlo a true . Después de que todos los suscriptores hayan
visto el evento generado, el componente FileSearcher examina el valor booleano y toma medidas.
El segundo patrón solo debería cancelar la operación si todos los suscriptores quieren que se cancele. En este
patrón, el nuevo campo se inicializa para indicar que se debe cancelar la operación y cualquier suscriptor puede
modificarlo para indicar que la operación debe continuar. Después de que todos los suscriptores hayan visto el
evento generado, el componente FileSearcher examina el valor booleano y toma medidas. Hay un paso
adicional en este patrón: el componente necesita saber si los suscriptores vieron el evento. Si no hay ningún
suscriptor, el campo indicaría incorrectamente una cancelación.
Vamos a implementar la primera versión de este ejemplo. Debe agregar un campo booleano denominado
CancelRequested al tipo FileFoundArgs :
Este nuevo campo se inicializa automáticamente en false , el valor predeterminado de un campo booleano, por
lo que no se cancela accidentalmente. El otro cambio en el componente consiste en comprobar el indicador
después de generar el evento para ver si alguno de los suscriptores solicitó una cancelación:
Una ventaja de este patrón es que no supone un cambio brusco. Ninguno de los suscriptores solicitó una
cancelación antes y siguen sin hacerlo. No debe actualizarse el código de ningún suscriptor a menos que
quieran admitir el nuevo protocolo de cancelación. Está acoplado muy holgadamente.
Vamos a actualizar el suscriptor para que solicite una cancelación una vez que encuentra el primer ejecutable:
EventHandler<FileFoundArgs> onFileFound = (sender, eventArgs) =>
{
Console.WriteLine(eventArgs.FoundFile);
eventArgs.CancelRequested = true;
};
De nuevo, puede seguir las recomendaciones para crear un tipo de referencia inmutable para los argumentos de
evento.
Después, defina el evento. Esta vez, usará una sintaxis diferente. Además de usar la sintaxis de campos, puede
crear explícitamente la propiedad con controladores add y remove. En este ejemplo, no necesitará código
adicional en los controladores, pero aquí se muestra cómo se crean.
En muchos aspectos, el código que se escribe aquí refleja el código que genera el compilador para las
definiciones de evento de campo que se vieron anteriormente. El evento se crea mediante una sintaxis muy
similar a la que se usó para las propiedades. Tenga en cuenta que los controladores tienen nombres diferentes:
add y remove . Se llaman para suscribirse al evento o para cancelar la suscripción al evento. Tenga en cuenta
que también debe declarar un campo de respaldo privado para almacenar la variable de evento. Se inicializa en
null.
Después, se agregará la sobrecarga del método Search que recorre los subdirectorios y genera los dos eventos.
La manera más fácil de hacerlo consiste en usar un argumento predeterminado para especificar que se quiere
buscar en todos los directorios:
En este punto, puede ejecutar la aplicación mediante la llamada a la sobrecarga para buscar en todos los
subdirectorios. No hay ningún suscriptor en el nuevo evento ChangeDirectory , pero al usar el elemento
?.Invoke() se garantiza que esto funciona correctamente.
Vamos a agregar un controlador para escribir una línea que muestre el progreso en la ventana de la consola.
Ha visto los patrones que se siguen en todo el ecosistema de. NET. El aprendizaje de estos patrones y
convenciones le permitirá escribir elementos de C# y .NET rápidamente.
Más adelante verá algunos cambios en estos patrones en la versión más reciente de. NET.
Siguiente
Patrón de eventos actualizado de .NET Core
16/09/2021 • 4 minutes to read
Anterior
En el artículo anterior se describían los patrones de eventos más comunes. .NET Core tiene un patrón menos
estricto. En esta versión, la definición EventHandler<TEventArgs> ya no tiene la restricción que obliga a que
TEventArgs sea una clase derivada de System.EventArgs .
Esto aumenta la flexibilidad y es compatible con versiones anteriores. Comencemos con la flexibilidad. La clase
System.EventArgs introduce un método, MemberwiseClone() , que crea una copia superficial del objeto. Dicho
método debe usar la reflexión para implementar su función en cualquier clase derivada de EventArgs . Esta
funcionalidad es más fácil de crear en una clase derivada concreta. Esto significa que derivar de
System.EventArgs es una restricción que limita los diseños, pero no proporciona ninguna ventaja adicional. De
hecho, puede cambiar las definiciones de FileFoundArgs y SearchDirectoryArgs para que no deriven de
EventArgs . El programa funcionará exactamente igual.
El cambio adicional consiste en llamar al constructor sin parámetros antes de entrar en el constructor que
inicializa todos los campos. Sin esta adición, las reglas de C# informarán de que se está teniendo acceso a las
propiedades antes de que se hayan asignado.
No debe cambiar FileFoundArgs de una clase (tipo de referencia) a un struct (tipo de valor). Esto se debe a que
el protocolo para controlar la cancelación requiere que los argumentos de evento se pasen por referencia. Si
realizase el mismo cambio, la clase de búsqueda de archivos no podría observar nunca los cambios realizados
por ninguno de los suscriptores de eventos. Se usaría una nueva copia de la estructura para cada suscriptor, y
dicha copia sería diferente de la que ve el objeto de búsqueda de archivos.
Ahora veamos cómo este cambio puede ser compatible con versiones anteriores. La eliminación de la
restricción no afecta al código existente. Los tipos de argumento de evento existentes siguen derivando de
System.EventArgs . La compatibilidad con versiones anteriores es uno de los motivos principales por los que
siguen derivando de System.EventArgs . Los suscriptores de eventos existentes serán suscriptores a un evento
que haya seguido el patrón clásico.
Según una lógica similar, cualquier tipo de argumento de evento creado ahora no tendría ningún suscriptor en
el código base existente. Los nuevos tipos de evento que no deriven de System.EventArgs no interrumpirán ese
código base.
Eventos con suscriptores Async
Le queda un último patrón que aprender: Cómo escribir correctamente suscriptores de eventos que llaman a
código asincrónico. Este reto se describe en el artículo sobre async y await. Los métodos asincrónicos pueden
tener un tipo de valor devuelto void, pero esto no es recomendable. Cuando el código de suscriptor de eventos
llama a un método asincrónico, no le queda otra opción que crear un método async void , ya que lo requiere la
firma del controlador de eventos.
Debe conciliar estas instrucciones contradictorias. De alguna manera, debe crear un método async void seguro.
A continuación se muestran los aspectos básicos del patrón que debe implementar:
En primer lugar, observe que el controlador está marcado como un controlador asincrónico. Dado que se va a
asignar a un tipo de delegado de controlador de eventos, tendrá un tipo de valor devuelto void. Esto significa
que debe seguir el patrón que se muestra en el controlador y no debe permitir que se produzca ninguna
excepción fuera del contexto del controlador asincrónico. Como no devuelve una tarea, no hay ninguna tarea
que pueda notificar el error entrando en el estado de error. Dado que el método es asincrónico, no puede
producir la excepción. (El método de llamada ha continuado con la ejecución porque es async ). El
comportamiento real en tiempo de ejecución se definirá de forma diferente para diferentes entornos. Se puede
terminar el subproceso o el proceso que posee el subproceso, o dejar el proceso en un estado indeterminado.
Todas estas salidas potenciales son altamente no deseables.
Por eso debe encapsular la instrucción await para la tarea asincrónica en su propio bloque try. Si esto genera
una tarea con error, puede registrar el error. Si se produce un error del que no se puede recuperar la aplicación,
puede salir del programa de forma rápida y correctamente.
Estas son las principales actualizaciones del patrón de eventos de .NET. Verá numerosos ejemplos de las
versiones anteriores de las bibliotecas con las que trabaje. Aun así, también debe entender los patrones más
recientes.
El siguiente artículo de esta serie le ayudará a distinguir entre el uso de delegates y events en los diseños.
Dado que se trata de conceptos similares, el artículo le ayudará a tomar la mejor decisión para sus programas.
Siguiente
Distinción de delegados y eventos
16/09/2021 • 3 minutes to read
Anterior
Los desarrolladores que son nuevos en la plataforma de NET Core a menudo tienen problemas para decidir
entre un diseño basado en delegates y uno basado en events . La elección de delegados o eventos suele ser
difícil, ya que las dos características de lenguaje son similares. Los eventos incluso se crean con compatibilidad
de lenguaje para los delegados.
Ambos ofrecen un escenario de enlace en tiempo de ejecución: permiten escenarios donde un componente se
comunica mediante una llamada a un método que solo se conoce en tiempo de ejecución. Ambos admiten
métodos de suscriptor único y múltiple. Puede que se haga referencia a estos términos como compatibilidad
con multidifusión o de conversión única. Ambos admiten una sintaxis similar para agregar y quitar
controladores. Por último, para generar un evento y llamar a un delegado se usa exactamente la misma sintaxis
de llamada de método. Incluso los dos admiten la misma sintaxis del método Invoke() para su uso con el
operador ?. .
Con todas estas similitudes, es fácil tener problemas para determinar cuándo usar cada uno.
Evaluar cuidadosamente
Las consideraciones anteriores no son reglas rápidas ni estrictas. En su lugar, representan instrucciones que
pueden ayudarle a decidir qué opción es mejor para su uso particular. Como son similares, incluso puede crear
un prototipo de los dos y considerar con cuál sería más natural trabajar. Ambos controlan escenarios de enlace
en tiempo de ejecución correctamente. Use el que comunique mejor su diseño.
Language-Integrated Query (LINQ)
16/09/2021 • 3 minutes to read
class LINQQueryExpressions
{
static void Main()
{
Pasos siguientes
Para obtener más información sobre LINQ, empiece a familiarizarse con algunos conceptos básicos en
Conceptos básicos de las expresiones de consultas y, después, lea la documentación de la tecnología de LINQ en
la que esté interesado:
Documentos XML: LINQ to XML
ADO.NET Entity Framework: LINQ to Entities
Colecciones .NET, archivos y cadenas, entre otros: LINQ to Objects
Para comprender mejor los aspectos generales de LINQ, vea LINQ in C# (LINQ en C#).
Para empezar a trabajar con LINQ en C#, vea el tutorial Trabajar con LINQ.
Conceptos básicos de las expresiones de consultas
16/09/2021 • 13 minutes to read
En este artículo se presentan los conceptos básicos relacionados con las expresiones de consulta en C#.
IEnumerable<int> highScoresQuery =
from score in scores
where score > 80
orderby score descending
select score;
Recuperar una secuencia de elementos como en el ejemplo anterior, pero transformándolos en un nuevo
tipo de objeto. Por ejemplo, una consulta puede recuperar solo los apellidos de ciertos registros de
clientes de un origen de datos. También puede recuperar el registro completo y, luego, usarlo para
construir otro tipo de objeto en memoria, o incluso datos XML, antes de generar la secuencia de
resultado final. En el ejemplo siguiente muestra una proyección de int a string . Observe el nuevo tipo
de highScoresQuery .
IEnumerable<string> highScoresQuery2 =
from score in scores
where score > 80
orderby score descending
select $"The score is {score}";
int highScoreCount =
(from score in scores
where score > 80
select score)
.Count();
En el ejemplo anterior, observe el uso de los paréntesis alrededor de la expresión de consulta antes
de llamar al método Count . Esto también se puede expresar mediante una nueva variable para
almacenar el resultado concreto. Esta técnica es más legible porque hace que la variable que
almacena la consulta se mantenga separada de la consulta que almacena un resultado.
IEnumerable<int> highScoresQuery3 =
from score in scores
where score > 80
select score;
En el ejemplo anterior, la consulta se ejecuta en la llamada a Count , ya que Count debe iterar los resultados
para determinar el número de elementos devueltos por highScoresQuery .
En el ejemplo de código siguiente se muestra una expresión de consulta simple con un origen de datos, una
cláusula de filtrado, una cláusula de clasificación y ninguna transformación en los elementos de origen. La
cláusula select finaliza la consulta.
static void Main()
{
// Data source.
int[] scores = { 90, 71, 82, 93, 75, 82 };
// Query Expression.
IEnumerable<int> scoreQuery = //query variable
from score in scores //required
where score > 80 // optional
orderby score descending // optional
select score; //must end with select or group
En el ejemplo anterior, scoreQuery es una variable de consulta, que a veces se conoce simplemente como una
consulta. La variable de consulta no almacena datos de resultado reales, que se producen en el bucle foreach .
Cuando se ejecuta la instrucción foreach , los resultados de la consulta no se devuelven a través de la variable
de consulta scoreQuery , sino a través de la variable de iteración testScore . La variable scoreQuery se puede
iterar en un segundo bucle foreach . Siempre y cuando ni esta ni el origen de datos se hayan modificado,
producirá los mismos resultados.
Una variable de consulta puede almacenar una consulta expresada en sintaxis de consulta, en sintaxis de
método o en una combinación de ambas. En los ejemplos siguientes, queryMajorCities y queryMajorCities2
son variables de consulta:
//Query syntax
IEnumerable<City> queryMajorCities =
from city in cities
where city.Population > 100000
select city;
// Method-based syntax
IEnumerable<City> queryMajorCities2 = cities.Where(c => c.Population > 100000);
Por otro lado, en los dos ejemplos siguientes se muestran variables que no son de consulta, a pesar de que se
inicialicen con una consulta. No son variables de consulta porque almacenan resultados:
int highestScore =
(from score in scores
select score)
.Max();
List<City> largeCitiesList =
(from country in countries
from city in country.Cities
where city.Population > 10000
select city)
.ToList();
Para obtener más información sobre las distintas formas de expresar consultas, vea Query syntax and method
syntax in LINQ (Sintaxis de consulta y sintaxis de método en LINQ).
Asignación implícita y explícita de tipos de variables de consulta
En esta documentación se suele proporcionar el tipo explícito de la variable de consulta para mostrar las
relaciones de tipo entre la variable de consulta y la cláusula select. Pero también se puede usar la palabra clave
var para indicarle al compilador que infiera el tipo de una variable de consulta (u otra variable local) en tiempo
de compilación. Por ejemplo, la consulta de ejemplo que se mostró anteriormente en este tema también se
puede expresar mediante la asignación implícita de tipos:
Para obtener más información, vea Implicitly typed local variables (Variables locales con asignación implícita de
tipos) y Type relationships in LINQ query operations (Relaciones entre tipos en las operaciones de consulta de
LINQ).
Iniciar una expresión de consulta
Una expresión de consulta debe comenzar con una cláusula from , que especifica un origen de datos junto con
una variable de rango. La variable de rango representa cada elemento sucesivo de la secuencia de origen a
medida que esta se recorre. La variable de rango está fuertemente tipada en función del tipo de elementos del
origen de datos. En el ejemplo siguiente, como countries es una matriz de objetos Country , la variable de
rango también está tipada como Country . Dado que la variable de rango está fuertemente tipada, se puede
usar el operador punto para tener acceso a cualquier miembro disponible del tipo.
IEnumerable<Country> countryAreaQuery =
from country in countries
where country.Area > 500000 //sq km
select country;
La variable de rango está en el ámbito hasta que se cierra la consulta con un punto y coma o con una cláusula
de continuación.
Una expresión de consulta puede contener varias cláusulas from . Use más cláusulas from cuando cada
elemento de la secuencia de origen sea una colección en sí mismo o contenga una colección. Por ejemplo,
supongamos que tiene una colección de objetos Country , cada uno de los cuales contiene una colección de
objetos City denominados Cities . Para consultar los objetos City de cada Country , use dos cláusulas from
, como se muestra aquí:
IEnumerable<City> cityQuery =
from country in countries
from city in country.Cities
where city.Population > 10000
select city;
var queryCountryGroups =
from country in countries
group country by country.Name[0];
Para obtener más información sobre la agrupación, vea group clause (Cláusula group).
select (cláusula)
Use la cláusula select para generar todos los demás tipos de secuencias. Una cláusula select simple solo
genera una secuencia del mismo tipo de objetos que los objetos contenidos en el origen de datos. En este
ejemplo, el origen de datos contiene objetos Country . La cláusula orderby simplemente ordena los elementos
con un orden nuevo y la cláusula select genera una secuencia con los objetos Country reordenados.
IEnumerable<Country> sortedQuery =
from country in countries
orderby country.Area
select country;
La cláusula select puede usarse para transformar los datos de origen en secuencias de nuevos tipos. Esta
transformación también se denomina proyección. En el ejemplo siguiente, la cláusula select proyecta una
secuencia de tipos anónimos que solo contiene un subconjunto de los campos del elemento original. Tenga en
cuenta que los nuevos objetos se inicializan mediante un inicializador de objeto.
// Here var is required because the query
// produces an anonymous type.
var queryNameAndPop =
from country in countries
select new { Name = country.Name, Pop = country.Population };
Para obtener más información sobre todas las formas en que se puede usar una cláusula select para
transformar datos de origen, vea select clause (Cláusula select).
Continuaciones con into
Puede usar la palabra clave into en una cláusula select o group para crear un identificador temporal que
almacene una consulta. Hágalo cuando deba realizar operaciones de consulta adicionales en una consulta
después de una operación de agrupación o selección. En el siguiente ejemplo se agrupan los objetos countries
según su población en intervalos de 10 millones. Una vez que se han creado estos grupos, las cláusulas
adicionales filtran algunos grupos y, después, ordenan los grupos en orden ascendente. Para realizar esas
operaciones adicionales, es necesaria la continuación representada por countryGroup .
IEnumerable<City> queryCityPop =
from city in cities
where city.Population < 200000 && city.Population > 100000
select city;
var categoryQuery =
from cat in categories
join prod in products on cat equals prod.Category
select new { Category = cat, Name = prod.Name };
También puede realizar una combinación agrupada. Para ello, almacene los resultados de la operación join en
una variable temporal mediante el uso de la palabra clave into. Para obtener más información, vea join
(Cláusula, Referencia de C#).
let (cláusula)
Use la cláusula let para almacenar el resultado de una expresión, como una llamada de método, en una nueva
variable de rango. En el ejemplo siguiente, la variable de rango firstName almacena el primer elemento de la
matriz de cadenas devuelta por Split .
string[] names = { "Svetlana Omelchenko", "Claire O'Donnell", "Sven Mortensen", "Cesar Garcia" };
IEnumerable<string> queryFirstNames =
from name in names
let firstName = name.Split(' ')[0]
select firstName;
Para más información, consulte Realizar una subconsulta en una operación de agrupación.
Vea también
Guía de programación de C#
Language-Integrated Query (LINQ)
Palabras clave de consultas (LINQ)
Información general sobre operadores de consulta estándar
LINQ en C#
16/09/2021 • 2 minutes to read
Esta sección contiene vínculos a temas que ofrecen información más detallada sobre LINQ.
En esta sección
Introducción a las consultas LINQ
Describe las tres partes de la operación de consulta LINQ básica comunes a todos los lenguajes y orígenes de
datos.
LINQ y tipos genéricos
Ofrece una breve introducción a los tipos genéricos, tal como se usan en LINQ.
Transformaciones de datos con LINQ
Describe las diversas maneras de transformar datos recuperados en las consultas.
Relaciones entre tipos en las operaciones de consulta LINQ
Describe cómo se mantienen o transforman los tipos en las tres partes de una operación de consulta LINQ.
Sintaxis de consulta y sintaxis de método en LINQ
Compara la sintaxis de método y la sintaxis de consulta como dos maneras de expresar una consulta LINQ.
Características de C# compatibles con LINQ
Describe las construcciones de lenguaje de C# compatibles con LINQ.
Secciones relacionadas
Expresiones de consulta LINQ
Incluye información general sobre las consultas en LINQ y proporciona vínculos a recursos adicionales.
Información general sobre operadores de consulta estándar
Presenta los métodos estándar usados en LINQ.
Escribir consultas LINQ en C#
16/09/2021 • 4 minutes to read
En este artículo se muestran las tres formas de escribir una consulta LINQ en C#:
1. Usar sintaxis de consulta.
2. Usar sintaxis de método.
3. Usar una combinación de sintaxis de consulta y sintaxis de método.
Los ejemplos siguientes muestran algunas consultas LINQ sencillas mediante cada enfoque enumerado
anteriormente. En general, la regla es usar (1) siempre que sea posible y usar (2) y (3) cuando sea necesario.
NOTE
Estas consultas funcionan en colecciones en memoria simples, pero la sintaxis básica es idéntica a la empleada en LINQ to
Entities y LINQ to XML.
// Query #1.
List<int> numbers = new List<int>() { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
// Query #2.
IEnumerable<int> orderingQuery =
from num in numbers
where num < 3 || num > 7
orderby num ascending
select num;
// Query #3.
string[] groupingQuery = { "carrots", "cabbage", "broccoli", "beans", "barley" };
IEnumerable<IGrouping<char, string>> queryFoodGroups =
from item in groupingQuery
group item by item[0];
Tenga en cuenta que el tipo de las consultas es IEnumerable<T>. Todas estas consultas podrían escribirse
mediante var como se muestra en el ejemplo siguiente:
var query = from num in numbers...
En cada ejemplo anterior, las consultas no se ejecutan realmente hasta que se recorre en iteración la variable de
consulta en una instrucción foreach o cualquier otra instrucción. Para más información, vea Introduction to
LINQ Queries (Introducción a las consultas LINQ).
// Query #5.
IEnumerable<int> concatenationQuery = numbers1.Concat(numbers2);
Si el método tiene parámetros Action o Func, se proporcionan en forma de expresión lambda, como se muestra
en el ejemplo siguiente:
// Query #6.
IEnumerable<int> largeNumbersQuery = numbers2.Where(c => c > 15);
En las consultas anteriores, solo la número 4 se ejecuta inmediatamente. Esto se debe a que devuelve un valor
único, y no una colección IEnumerable<T> genérica. El propio método tiene que usar foreach para calcular su
valor.
Cada una de las consultas anteriores puede escribirse mediante tipos implícitos con var, como se muestra en el
ejemplo siguiente:
Dado que la consulta número 7 devuelve un solo valor y no una colección, se ejecuta inmediatamente.
La consulta anterior puede escribirse mediante tipos implícitos con var como sigue:
Vea también
Walkthrough: Writing Queries in C# (Tutorial: Escribir consultas en C#)
Language-Integrated Query (LINQ)
where (cláusula)
Consultar una colección de objetos
16/09/2021 • 2 minutes to read
En este ejemplo se muestra cómo realizar una consulta simple en una lista de objetos Student . Cada objeto
Student contiene información básica sobre el alumno y una lista que representa las puntuaciones del alumno
en cuatro exámenes.
Esta aplicación sirve de marco en muchos otros ejemplos de esta sección que usan el mismo origen de datos
students .
Ejemplo
La consulta siguiente devuelve los alumnos que reciben una puntuación de 90 o más en su primer examen.
Esta consulta es deliberadamente simple para permitirle experimentar. Por ejemplo, puede probar otras
condiciones en la cláusula where o usar una cláusula orderby para ordenar los resultados.
Vea también
Language-Integrated Query (LINQ)
Interpolación de cadenas
Cómo devolver una consulta desde un método
(Guía de programación de C#)
16/09/2021 • 2 minutes to read
En este ejemplo se muestra cómo devolver una consulta desde un método como un valor devuelto y como un
parámetro out .
Los objetos de consulta admiten composición, lo que significa que puede devolver una consulta desde un
método. Los objetos que representan consultas no almacenan la colección resultante, sino los pasos para
generar los resultados cuando sea necesario. La ventaja de devolver objetos de consulta desde métodos es que
se pueden componer o modificar todavía más. Por lo tanto, cualquier valor devuelto o parámetro out de un
método que devuelve una consulta también debe tener ese tipo. Si un método materializa una consulta en un
tipo concreto List<T> o Array, se considera que está devolviendo los resultados de la consulta en lugar de la
propia consulta. Una variable de consulta que se devuelve desde un método sigue pudiendo componerse o
modificarse.
Ejemplo
En el ejemplo siguiente, el primer método devuelve una consulta como un valor devuelto y el segundo método
devuelve una consulta como un parámetro out . Tenga en cuenta que, en ambos casos, se trata de una consulta
que se devuelve, no de los resultados de la consulta.
class MQ
{
// QueryMethhod1 returns a query as its value.
IEnumerable<string> QueryMethod1(ref int[] ints)
{
var intsToStrings = from i in ints
where i > 4
select i.ToString();
return intsToStrings;
}
int[] nums = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
IEnumerable<string> myQuery2;
// QueryMethod2 returns a query as the value of its out parameter.
app.QueryMethod2(ref nums, out myQuery2);
Vea también
Language-Integrated Query (LINQ)
Almacenar los resultados de una consulta en
memoria
16/09/2021 • 2 minutes to read
Una consulta es básicamente un conjunto de instrucciones sobre cómo recuperar y organizar los datos. Las
consultas se ejecutan de forma diferida, ya que se solicita cada elemento subsiguiente del resultado. Cuando se
usa foreach para iterar los resultados, los elementos se devuelven a medida que se tiene acceso a ellos. Para
evaluar una consulta y almacenar los resultados sin ejecutar un bucle foreach , simplemente llame a uno de los
métodos siguientes en la variable de consulta:
ToList
ToArray
ToDictionary
ToLookup
Recomendamos que, al almacenar los resultados de consulta, asigne el objeto de colección devuelto a una
nueva variable tal como se muestra en el ejemplo siguiente:
Ejemplo
class StoreQueryResults
{
static List<int> numbers = new List<int>() { 1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20 };
static void Main()
{
IEnumerable<int> queryFactorsOfFour =
from num in numbers
where num % 4 == 0
select num;
Vea también
Language-Integrated Query (LINQ)
Agrupar los resultados de consultas
16/09/2021 • 8 minutes to read
La agrupación es una de las capacidades más eficaces de LINQ. Los ejemplos siguientes muestran cómo agrupar
datos de varias maneras:
Por una sola propiedad.
Por la primera letra de una propiedad de cadena.
Por un intervalo numérico calculado.
Por un predicado booleano u otra expresión.
Por una clave compuesta.
Además, las dos últimas consultas proyectan sus resultados en un nuevo tipo anónimo que solo contiene el
nombre y los apellidos del estudiante. Para obtener más información, vea la cláusula group.
var queryFirstLetters =
from student in students
group student by student.LastName[0];
Pegue el método siguiente en la clase StudentClass . Cambie la instrucción de llamada en el método Main por
sc.GroupByRange() .
public void GroupByRange()
{
Console.WriteLine("\r\nGroup by numeric range and project into a new anonymous type:");
var queryNumericRange =
from student in students
let percentile = GetPercentile(student)
group new { student.FirstName, student.LastName } by percentile into percentGroup
orderby percentGroup.Key
select percentGroup;
/* Output:
Group and order by a compound key:
Name starts with A who scored more than 85
Terry Adams
Name starts with F who scored more than 85
Fadi Fakhouri
Hanying Feng
Name starts with G who scored more than 85
Cesar Garcia
Hugo Garcia
Name starts with G who scored less than 85
Debra Garcia
Name starts with M who scored more than 85
Sven Mortensen
Name starts with O who scored less than 85
Claire O'Donnell
Name starts with O who scored more than 85
Svetlana Omelchenko
Name starts with T who scored less than 85
Lance Tucker
Name starts with T who scored more than 85
Michael Tucker
Name starts with Z who scored more than 85
Eugene Zabokritski
*/
Vea también
GroupBy
IGrouping<TKey,TElement>
Language-Integrated Query (LINQ)
group (cláusula)
Tipos anónimos
Realizar una subconsulta en una operación de agrupación
Crear un grupo anidado
Agrupar datos
Crear un grupo anidado
16/09/2021 • 2 minutes to read
En el ejemplo siguiente se muestra cómo crear grupos anidados en una expresión de consulta LINQ. Cada grupo
creado a partir del nivel académico o del año de los estudiantes se subdivide en grupos según sus nombres.
Ejemplo
NOTE
Este ejemplo contiene referencias a objetos que se definen en el código de ejemplo de Query a collection of objects
(Consultar una colección de objetos).
public void QueryNestedGroups()
{
var queryNestedGroups =
from student in students
group student by student.Year into newGroup1
from newGroup2 in
(from student in newGroup1
group student by student.LastName)
group newGroup2 by newGroup1.Key;
Tenga en cuenta que se necesitan tres bucles foreach anidados para recorrer en iteración los elementos
internos de un grupo anidado.
Vea también
Language-Integrated Query (LINQ)
Realizar una subconsulta en una operación de
agrupación
16/09/2021 • 2 minutes to read
En este artículo se muestran dos maneras diferentes de crear una consulta que ordena los datos de origen en
grupos y, luego, realiza una subconsulta en cada grupo de forma individual. La técnica básica de cada ejemplo
consiste en agrupar los elementos de origen usando una continuación denominada newGroup y después
generar una nueva subconsulta en newGroup . Esta subconsulta se ejecuta en cada uno de los nuevos grupos
creados por la consulta externa. Tenga en cuenta que en este ejemplo concreto el resultado final no es un grupo,
sino una secuencia plana de tipos anónimos.
Para obtener más información sobre cómo agrupar, consulte Cláusula group.
Para obtener más información sobre continuaciones, consulte into. En el ejemplo siguiente se usa una estructura
de datos en memoria como origen de datos, pero se aplican los mismos principios para cualquier tipo de origen
de datos LINQ.
Ejemplo
NOTE
Este ejemplo contiene referencias a objetos que se definen en el código de ejemplo de Query a collection of objects
(Consultar una colección de objetos).
La consulta del fragmento de código anterior también se puede escribir con la sintaxis de método. El siguiente
fragmento de código tiene una consulta semánticamente equivalente escrita con sintaxis de método.
public void QueryMaxUsingMethodSyntax()
{
var queryGroupMax = students
.GroupBy(student => student.Year)
.Select(studentGroup => new
{
Level = studentGroup.Key,
HighestScore = studentGroup.Select(student2 => student2.ExamScores.Average()).Max()
});
Vea también
Language-Integrated Query (LINQ)
Agrupar resultados por claves contiguas
16/09/2021 • 7 minutes to read
En el ejemplo siguiente se muestra cómo agrupar elementos en fragmentos que representan subsecuencias de
claves contiguas. Por ejemplo, suponga que tiene la siguiente secuencia de pares clave-valor:
C L AVE VA LO R
A We
A think
A that
B Linq
C is
A really
B cool
B !
Ejemplo
En el ejemplo siguiente se muestran el método de extensión y el código de cliente que lo usa:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq;
namespace ChunkIt
{
// Static class to contain the extension methods.
public static class MyExtensions
{
public static IEnumerable<IGrouping<TKey, TSource>> ChunkBy<TSource, TKey>(this IEnumerable<TSource>
source, Func<TSource, TKey> keySelector)
{
return source.ChunkBy(keySelector, EqualityComparer<TKey>.Default);
}
// Make a new Chunk (group) object that initially has one GroupItem, which is a copy of the
current source element.
current = new Chunk<TKey, TSource>(key, enumerator, value => comparer.Equals(key,
keySelector(value)));
// Return the Chunk. A Chunk is an IGrouping<TKey,TSource>, which is the return value of the
ChunkBy method.
// At this point the Chunk only has the first element in its source sequence. The remaining
elements will be
// returned only when the client code foreach's over this chunk. See Chunk.GetEnumerator for
more info.
yield return current;
// Check to see whether (a) the chunk has made a copy of all its source elements or
// (b) the iterator has reached the end of the source sequence. If the caller uses an inner
// foreach loop to iterate the chunk items, and that loop ran to completion,
// then the Chunk.GetEnumerator method will already have made
// copies of all chunk items before we get here. If the Chunk.GetEnumerator loop did not
// enumerate all elements in the chunk, we need to do it here to avoid corrupting the
iterator
// for clients that may be calling us on a separate thread.
if (current.CopyAllChunkElements() == noMoreSourceElements)
{
yield break;
}
}
}
// A Chunk is a contiguous group of one or more source elements that have the same key. A Chunk
// has a key and a list of ChunkItem objects, which are copies of the elements in the source
sequence.
class Chunk<TKey, TSource> : IGrouping<TKey, TSource>
{
// INVARIANT: DoneCopyingChunk == true ||
// INVARIANT: DoneCopyingChunk == true ||
// (predicate != null && predicate(enumerator.Current) && current.Value == enumerator.Current)
// A Chunk has a linked list of ChunkItems, which represent the elements in the current chunk.
Each ChunkItem
// has a reference to the next ChunkItem in the list.
class ChunkItem
{
public ChunkItem(TSource value)
{
Value = value;
}
public readonly TSource Value;
public ChunkItem Next = null;
}
// Flag to indicate the source iterator has reached the end of the source sequence.
internal bool isLastSourceElement = false;
// The end and beginning are the same until the list contains > 1 elements.
tail = head;
// Indicates that all chunk elements have been copied to the list of ChunkItems,
// and the source enumerator is either at the end, or else on an element with a new key.
// the tail of the linked list is set to null in the CopyNextChunkElement method if the
// key of the next element does not match the current chunk's key, or there are no more elements
in the source.
private bool DoneCopyingChunk => tail == null;
// Called after the end of the last chunk was reached. It first checks whether
// there are more elements in the source sequence. If there are, it
// Returns true if enumerator for this chunk was exhausted.
internal bool CopyAllChunkElements()
{
while (true)
{
lock (m_Lock)
{
if (DoneCopyingChunk)
{
// If isLastSourceElement is false,
// it signals to the outer iterator
// to continue iterating.
return isLastSourceElement;
}
else
{
CopyNextChunkElement();
}
}
}
}
// Invoked by the inner foreach loop. This method stays just one step ahead
// of the client requests. It adds the next element of the chunk only after
// the clients requests the last element in the list so far.
public IEnumerator<TSource> GetEnumerator()
{
//Specify the initial element to enumerate.
ChunkItem current = head;
// A simple named type is used for easier viewing in the debugger. Anonymous types
// work just as well with the ChunkBy operator.
public class KeyValPair
{
public string Key { get; set; }
public string Value { get; set; }
}
class Program
{
// The source sequence.
public static IEnumerable<KeyValPair> list;
Para usar el método de extensión en el proyecto, copie la clase estática MyExtensions en un archivo de código
fuente nuevo o ya existente y, si es necesario, agregue una directiva using para el espacio de nombres donde
se encuentra.
Vea también
Language-Integrated Query (LINQ)
Especificar dinámicamente filtros con predicado en
tiempo de ejecución
16/09/2021 • 2 minutes to read
En algunos casos, no se conoce cuántos predicados hay que aplicar a los elementos de origen de la cláusula
where hasta el tiempo de ejecución. Una forma de especificar dinámicamente varios filtros con predicado es
usar el método Contains, como se muestra en el ejemplo siguiente. El ejemplo se construye de dos maneras. En
primer lugar, el proyecto se ejecuta al filtrar por los valores proporcionados en el programa. Luego se vuelve a
ejecutar mediante la entrada proporcionada en tiempo de ejecución.
QueryById(ids);
6. Ejecute el proyecto.
7. El resultado siguiente se muestra en una ventana de la consola:
Garcia: 114
O'Donnell: 112
Omelchenko: 111
8. El siguiente paso es volver a ejecutar el proyecto, esta vez mediante la entrada especificada en tiempo de
ejecución en lugar de la matriz ids . Cambie QueryByID(ids) por QueryByID(args) en el método Main .
9. Ejecute el proyecto con los argumentos de la línea de comandos 122 117 120 115 . Una vez ejecutado el
proyecto, esos valores se convierten en elementos de args , el parámetro del método Main .
10. El resultado siguiente se muestra en una ventana de la consola:
Adams: 120
Feng: 117
Garcia: 115
Tucker: 122
default:
break;
}
Console.WriteLine($"The following students are at level {year}");
foreach (Student name in studentQuery)
{
Console.WriteLine($"{name.LastName}: {name.ID}");
}
}
3. En el método Main , sustituya la llamada a QueryByID por la siguiente llamada, que envía el primer
elemento de la matriz args como su argumento: QueryByYear(args[0]) .
4. Ejecute el proyecto con un argumento de la línea de comandos de un valor entero entre 1 y 4.
Vea también
Language-Integrated Query (LINQ)
where (cláusula)
Realizar combinaciones internas
16/09/2021 • 11 minutes to read
En términos de la base de datos relacional, una combinación interna genera un conjunto de resultados en el que
cada elemento de la primera colección aparece una vez para cada elemento coincidente en la segunda colección.
Si un elemento de la primera colección no tiene ningún elemento coincidente, no aparece en el conjunto de
resultados. El método Join, que se llama mediante la cláusula join de C#, implementa una combinación
interna.
En este artículo se muestra cómo realizar cuatro variaciones de una combinación interna:
Una combinación interna simple que correlaciona elementos de dos orígenes de datos según una clave
simple.
Una combinación interna que correlaciona elementos de dos orígenes de datos según una clave
compuesta. Una clave compuesta, que es una clave formada por más de un valor, permite correlacionar
elementos en función de más de una propiedad.
Una combinación múltiple en la que las sucesivas operaciones de combinación se anexan entre sí.
Una combinación interna que se implementa mediante una combinación agrupada.
class Pet
{
public string Name { get; set; }
public Person Owner { get; set; }
}
/// <summary>
/// Simple inner join.
/// </summary>
public static void InnerJoinExample()
{
Person magnus = new Person { FirstName = "Magnus", LastName = "Hedlund" };
Person terry = new Person { FirstName = "Terry", LastName = "Adams" };
Person charlotte = new Person { FirstName = "Charlotte", LastName = "Weiss" };
Person arlene = new Person { FirstName = "Arlene", LastName = "Huff" };
Person rui = new Person { FirstName = "Rui", LastName = "Raposo" };
Tenga en cuenta que el objeto Person cuyo LastName es "Huff" no aparece en el conjunto de resultados porque
no hay ningún objeto Pet que tenga Pet.Owner igual que Person .
class Employee
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int EmployeeID { get; set; }
}
class Student
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int StudentID { get; set; }
}
/// <summary>
/// Performs a join operation using a composite key.
/// </summary>
public static void CompositeKeyJoinExample()
{
// Create a list of employees.
List<Employee> employees = new List<Employee> {
new Employee { FirstName = "Terry", LastName = "Adams", EmployeeID = 522459 },
new Employee { FirstName = "Charlotte", LastName = "Weiss", EmployeeID = 204467 },
new Employee { FirstName = "Magnus", LastName = "Hedland", EmployeeID = 866200 },
new Employee { FirstName = "Vernette", LastName = "Price", EmployeeID = 437139 } };
// Join the two data sources based on a composite key consisting of first and last name,
// to determine which employees are also students.
IEnumerable<string> query = from employee in employees
join student in students
on new { employee.FirstName, employee.LastName }
equals new { student.FirstName, student.LastName }
select employee.FirstName + " " + employee.LastName;
La segunda cláusula join de C# correlaciona los tipos anónimos devueltos por la primera combinación con
objetos Dog de la lista de perros proporcionada, según una clave compuesta formada por la propiedad Owner
de tipo Person y la primera letra del nombre del animal. Devuelve una secuencia de tipos anónimos que
contienen las propiedades Cat.Name y Dog.Name de cada par coincidente. Como se trata de una combinación
interna, solo se devuelven los objetos del primer origen de datos que tienen una correspondencia en el segundo
origen de datos.
class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
class Pet
{
public string Name { get; set; }
public Person Owner { get; set; }
}
Dog fourwheeldrive = new Dog { Name = "Four Wheel Drive", Owner = phyllis };
Dog duke = new Dog { Name = "Duke", Owner = magnus };
Dog denim = new Dog { Name = "Denim", Owner = terry };
Dog wiley = new Dog { Name = "Wiley", Owner = charlotte };
Dog snoopy = new Dog { Name = "Snoopy", Owner = rui };
Dog snickers = new Dog { Name = "Snickers", Owner = arlene };
// The first join matches Person and Cat.Owner from the list of people and
// cats, based on a common Person. The second join matches dogs whose names start
// with the same letter as the cats that have the same owner.
var query = from person in people
join cat in cats on person equals cat.Owner
join dog in dogs on
new { Owner = person, Letter = cat.Name.Substring(0, 1) }
equals new { dog.Owner, Letter = dog.Name.Substring(0, 1) }
select new { CatName = cat.Name, DogName = dog.Name };
class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
class Pet
{
public string Name { get; set; }
public Person Owner { get; set; }
}
/// <summary>
/// Performs an inner join by using GroupJoin().
/// </summary>
public static void InnerGroupJoinExample()
{
Person magnus = new Person { FirstName = "Magnus", LastName = "Hedlund" };
Person terry = new Person { FirstName = "Terry", LastName = "Adams" };
Person charlotte = new Person { FirstName = "Charlotte", LastName = "Weiss" };
Person arlene = new Person { FirstName = "Arlene", LastName = "Huff" };
Consulte también
Join
GroupJoin
Realizar combinaciones agrupadas
Realizar operaciones de combinación externa izquierda
Tipos anónimos (Guía de programación de C#).
Realizar combinaciones agrupadas
16/09/2021 • 5 minutes to read
La combinación agrupada resulta útil para generar estructuras de datos jerárquicas. Empareja cada elemento de
la primera colección con un conjunto de elementos correlacionados de la segunda colección.
Por ejemplo, una clase o una tabla de base de datos relacional denominada Student podría contener dos
campos: Id y Name . Una segunda clase o tabla de base de datos relacional denominada Course podría
contener dos campos: StudentId y CourseTitle . Una combinación agrupada de estos dos orígenes de datos,
basada en la combinación de Student.Id y Course.StudentId , agruparía cada Student con una colección de
objetos Course (que podrían estar vacíos).
NOTE
Cada elemento de la primera colección aparece en el conjunto de resultados de una combinación agrupada,
independientemente de si se encuentran elementos correlacionados en la segunda colección. En el caso de que no se
encuentren elementos correlacionados, la secuencia de elementos correlacionados para ese elemento estaría vacía. Por
consiguiente, el selector de resultados tiene acceso a cada uno de los elementos de la primera colección. Esto difiere del
selector de resultados en una combinación no agrupada, que no puede acceder a los elementos de la primera colección
que no tienen ninguna coincidencia en la segunda colección.
WARNING
Enumerable.GroupJoin no tiene ningún equivalente directo en términos de base de datos relacional tradicional. Sin
embargo, este método implementa un superconjunto de combinaciones internas y combinaciones externas izquierdas.
Ambas operaciones se pueden escribir en términos de una combinación agrupada. Para obtener más información,
consulte Operaciones de combinación y Entity Framework Core, GroupJoin.
En el primer ejemplo de este artículo se muestra cómo realizar una combinación agrupada. En el segundo
ejemplo se muestra cómo usar una combinación agrupada para crear elementos XML.
class Pet
{
public string Name { get; set; }
public Person Owner { get; set; }
}
/// <summary>
/// This example performs a grouped join.
/// </summary>
public static void GroupJoinExample()
{
Person magnus = new Person { FirstName = "Magnus", LastName = "Hedlund" };
Person terry = new Person { FirstName = "Terry", LastName = "Adams" };
Person charlotte = new Person { FirstName = "Charlotte", LastName = "Weiss" };
Person arlene = new Person { FirstName = "Arlene", LastName = "Huff" };
class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
class Pet
{
public string Name { get; set; }
public Person Owner { get; set; }
}
/// <summary>
/// This example creates XML output from a grouped join.
/// </summary>
public static void GroupJoinXMLExample()
{
Person magnus = new Person { FirstName = "Magnus", LastName = "Hedlund" };
Person terry = new Person { FirstName = "Terry", LastName = "Adams" };
Person charlotte = new Person { FirstName = "Charlotte", LastName = "Weiss" };
Person arlene = new Person { FirstName = "Arlene", LastName = "Huff" };
// Create XML to display the hierarchical organization of people and their pets.
XElement ownersAndPets = new XElement("PetOwners",
from person in people
join pet in pets on person equals pet.Owner into gj
select new XElement("Person",
new XAttribute("FirstName", person.FirstName),
new XAttribute("LastName", person.LastName),
from subpet in gj
select new XElement("Pet", subpet.Name)));
Console.WriteLine(ownersAndPets);
}
Vea también
Join
GroupJoin
Realizar combinaciones internas
Realizar operaciones de combinación externa izquierda
Tipos anónimos (Guía de programación de C#).
Realizar operaciones de combinación externa
izquierda
16/09/2021 • 2 minutes to read
Una combinación externa izquierda es una combinación en la que se devuelve cada elemento de la primera
colección, independientemente de si tiene elementos correlacionados en la segunda colección. Puede usar LINQ
para realizar una combinación externa izquierda llamando al método DefaultIfEmpty en los resultados de una
combinación agrupada.
Ejemplo
En el ejemplo siguiente se muestra cómo usar el método DefaultIfEmpty en los resultados de una combinación
agrupada para realizar una combinación externa izquierda.
El primer paso para generar una combinación externa izquierda de dos colecciones consiste en realizar una
combinación interna usando una combinación agrupada. (Consulte Realizar combinaciones internas para
obtener una explicación de este proceso). En este ejemplo, la lista de objetos Person está combinada
internamente con la lista de objetos Pet en función de un objeto Person que coincide con Pet.Owner .
El segundo paso consiste en incluir cada elemento de la primera colección (izquierda) en el conjunto de
resultados, incluso cuando no haya coincidencias en la colección derecha. Esto se realiza llamando a
DefaultIfEmpty en cada secuencia de elementos coincidentes de la combinación agrupada. En este ejemplo, se
llama a DefaultIfEmpty en cada secuencia de objetos Pet coincidentes. El método devuelve una colección que
contiene un único, valor predeterminado si la secuencia de objetos Pet coincidentes está vacía para cualquier
objeto Person , con lo que cada objeto Person se representa en la colección de resultados.
NOTE
El valor predeterminado para un tipo de referencia es null ; por consiguiente, el ejemplo busca una referencia NULL
antes de tener acceso a cada elemento de cada colección de Pet .
class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
class Pet
{
public string Name { get; set; }
public Person Owner { get; set; }
}
Vea también
Join
GroupJoin
Realizar combinaciones internas
Realizar combinaciones agrupadas
Tipos anónimos (Guía de programación de C#).
Ordenar los resultados de una cláusula join
16/09/2021 • 2 minutes to read
En este ejemplo se muestra cómo ordenar los resultados de una operación de combinación. Observe que la
ordenación se realiza después de la combinación. Aunque puede usar una cláusula orderby con una o varias de
las secuencias de origen antes de la combinación, normalmente no se recomienda. Algunos proveedores LINQ
podrían no conservar esa ordenación después de la combinación.
Ejemplo
Esta consulta crea una combinación agrupada y luego ordena los grupos según el elemento categoría, que
todavía está en el ámbito. Dentro del inicializador de tipo anónimo, una subconsulta ordena todos los elementos
coincidentes de la secuencia de productos.
class HowToOrderJoins
{
#region Data
class Product
{
public string Name { get; set; }
public int CategoryID { get; set; }
}
class Category
{
public string Name { get; set; }
public int ID { get; set; }
}
void OrderJoin1()
{
var groupJoinQuery2 =
from category in categories
join prod in products on category.ID equals prod.CategoryID into prodGroup
orderby category.Name
select new
{
Category = category.Name,
Products = from prod2 in prodGroup
orderby prod2.Name
select prod2
};
Vea también
Language-Integrated Query (LINQ)
orderby (cláusula)
join (cláusula)
Realizar una unión usando claves compuestas
16/09/2021 • 2 minutes to read
En este ejemplo se muestra cómo realizar operaciones de combinación en las que se quiere usar más de una
clave para definir a una coincidencia. Esto se logra mediante una clave compuesta. Una clave compuesta se crea
como un tipo anónimo o un nombre con tipo con los valores que se quieren comparar. Si la variable de consulta
se pasará a través de límites de método, use un tipo con nombre que invalida Equals y GetHashCode para la
clave. Los nombres de las propiedades y el orden en que aparecen deben ser idénticos en cada clave.
Ejemplo
En el ejemplo siguiente se muestra cómo usar una clave compuesta para combinar datos de tres tablas:
La inferencia de tipos en las claves compuestas depende de los nombres de las propiedades de las claves y el
orden en que aparecen. Si las propiedades de las secuencias de origen no tienen los mismos nombres, deberá
asignar nombres nuevos en las claves. Por ejemplo, si la tabla Orders y la tabla OrderDetails usan nombres
diferentes para las columnas, se podrían crear claves compuestas asignando nombres idénticos a los tipos
anónimos:
Vea también
Language-Integrated Query (LINQ)
join (cláusula)
group (cláusula)
Realizar operaciones de combinación
personalizadas
16/09/2021 • 5 minutes to read
En este ejemplo se muestra cómo realizar operaciones de combinación que no son posibles con la cláusula
join . En una expresión de consulta, la cláusula join se limita a combinaciones de igualdad (y se optimiza para
estas), que son con diferencia el tipo más común de operación de combinación. Al realizar una combinación de
igualdad, probablemente obtendrá siempre el mejor rendimiento con la cláusula join .
En cambio, la cláusula join no se puede usar en los siguientes casos:
Cuando la combinación se basa en una expresión de desigualdad (una combinación que no es de
igualdad).
Cuando la combinación se basa en más de una expresión de igualdad o desigualdad.
Cuando tenga que introducir una variable de rango temporal para la secuencia de la derecha (interior)
antes de la operación de combinación.
Para realizar combinaciones que no son de igualdad, puede usar varias cláusulas from para presentar cada
origen de datos de forma independiente. Después, aplique una expresión de predicado de una cláusula where a
la variable de rango para cada origen. La expresión también puede adoptar la forma de una llamada de método.
NOTE
No confunda este tipo de operación de combinación personalizada con el uso de varias cláusulas from para acceder a
colecciones internas. Para obtener más información, vea join (Cláusula, Referencia de C#).
Ejemplo 1
En el primer método del siguiente ejemplo se muestra una combinación cruzada sencilla. Las combinaciones
cruzadas se deben usar con precaución porque pueden generar conjuntos de resultados muy grandes. En
cambio, pueden resultar útiles en algunos escenarios para crear secuencias de origen en las que se ejecutan
consultas adicionales.
El segundo método genera una secuencia de todos los productos cuyo identificador de categoría aparece en la
lista de categorías en el lado izquierdo. Observe el uso de la cláusula let y el método Contains para crear una
matriz temporal. También se puede crear la matriz antes de la consulta y eliminar la primera cláusula from .
class CustomJoins
{
#region Data
class Product
{
public string Name { get; set; }
public int CategoryID { get; set; }
}
class Category
{
public string Name { get; set; }
public int ID { get; set; }
}
void CrossJoin()
{
var crossJoinQuery =
from c in categories
from p in products
select new { c.ID, p.Name };
void NonEquijoin()
{
var nonEquijoinQuery =
from p in products
let catIds = from c in categories
select c.ID
where catIds.Contains(p.CategoryID) == true
select new { Product = p.Name, CategoryID = p.CategoryID };
Console.WriteLine("Non-equijoin query:");
foreach (var v in nonEquijoinQuery)
{
Console.WriteLine($"{v.CategoryID,-5}{v.Product}");
}
}
}
/* Output:
Cross Join Query:
1 Tea
1 Mustard
1 Pickles
1 Carrots
1 Bok Choy
1 Peaches
1 Melons
1 Ice Cream
1 Mackerel
2 Tea
2 Mustard
2 Pickles
2 Carrots
2 Bok Choy
2 Peaches
2 Melons
2 Ice Cream
2 Mackerel
3 Tea
3 Mustard
3 Pickles
3 Carrots
3 Bok Choy
3 Peaches
3 Melons
3 Ice Cream
3 Mackerel
Non-equijoin query:
1 Tea
2 Mustard
2 Pickles
3 Carrots
3 Bok Choy
Press any key to exit.
*/
Ejemplo 2
En el ejemplo siguiente, la consulta debe combinar dos secuencias basándose en las claves coincidentes que, en
el caso de la secuencia interna (lado derecho), no se pueden obtener antes de la propia cláusula de combinación.
Si esta combinación se realizara con una cláusula join , se tendría que llamar al método Split para cada
elemento. El uso de varias cláusulas from permite a la consulta evitar la sobrecarga que supone la llamada al
método repetida. En cambio, puesto que join está optimizada, en este caso particular puede que siga
resultando más rápido que usar varias cláusulas from . Los resultados variarán dependiendo principalmente de
cuántos recursos requiera la llamada de método.
class MergeTwoCSVFiles
{
static void Main()
{
// See section Compiling the Code for information about the data files.
string[] names = System.IO.File.ReadAllLines(@"../../../names.csv");
string[] scores = System.IO.File.ReadAllLines(@"../../../scores.csv");
class Student
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int ID { get; set; }
public List<int> ExamScores { get; set; }
}
/* Output:
The average score of Omelchenko Svetlana is 82.5.
The average score of O'Donnell Claire is 72.25.
The average score of Mortensen Sven is 84.5.
The average score of Garcia Cesar is 88.25.
The average score of Garcia Debra is 67.
The average score of Fakhouri Fadi is 92.25.
The average score of Feng Hanying is 88.
The average score of Garcia Hugo is 85.75.
The average score of Tucker Lance is 81.75.
The average score of Adams Terry is 85.25.
The average score of Zabokritski Eugene is 83.
The average score of Tucker Michael is 92.
*/
Vea también
Language-Integrated Query (LINQ)
join (cláusula)
Ordenar los resultados de una cláusula join
Controlar valores nulos en expresiones de consulta
16/09/2021 • 2 minutes to read
En este ejemplo se muestra cómo controlar los posibles valores nulos en colecciones de origen. Una colección
de objetos como IEnumerable<T> puede contener elementos cuyo valor es NULL. Si una colección de origen es
null o contiene un elemento cuyo valor es null , y la consulta no controla los valores null , se iniciará un
elemento NullReferenceException cuando se ejecute la consulta.
Se pueden codificar de forma defensiva para evitar una excepción de referencia nula, tal y como se muestra en
el ejemplo siguiente:
var query1 =
from c in categories
where c != null
join p in products on c.ID equals
p?.CategoryID
select new { Category = c.Name, Name = p.Name };
En el ejemplo anterior, la cláusula where filtra todos los elementos nulos de la secuencia de categorías. Esta
técnica es independiente de la comprobación de null en la cláusula join. La expresión condicional con NULL de
este ejemplo funciona porque Products.CategoryID es de tipo int? , que es una abreviatura de Nullable<int> .
En una cláusula join, si solo una de las claves de comparación es de un tipo que acepta valores NULL, puede
convertir la otra en un tipo que acepta valores NULL en la expresión de consulta. En el ejemplo siguiente,
suponga que EmployeeID es una columna que contiene valores de tipo int? :
En cada uno de los ejemplos, se usa la palabra clave de consulta equals . C# 9 agrega una coincidencia de
patrones, que incluye patrones para is null y is not null . Estos patrones no se recomiendan en las consultas
LINQ porque es posible que los proveedores de consultas no interpreten correctamente la sintaxis nueva de C#.
Un proveedor de consultas es una biblioteca que traduce expresiones de consulta de C# a un formato de datos
nativo, como Entity Framework Core. Los proveedores de consultas implementan la interfaz
System.Linq.IQueryProvider para crear orígenes de datos que implementan la interfaz
System.Linq.IQueryable<T>.
Vea también
Nullable<T>
Language-Integrated Query (LINQ)
Tipos de valores que aceptan valores NULL
Controlar excepciones en expresiones de consulta
16/09/2021 • 2 minutes to read
Es posible llamar a cualquier método en el contexto de una expresión de consulta. Pero se recomienda evitar
llamar a cualquier método en una expresión de consulta que pueda crear un efecto secundario, como modificar
el contenido del origen de datos o producir una excepción. En este ejemplo se muestra cómo evitar que se
produzcan excepciones al llamar a métodos en una expresión de consulta sin infringir las instrucciones
generales de .NET sobre el control de excepciones. En esas instrucciones se indica que es aceptable detectar una
excepción concreta cuando se comprende por qué se produce en un contexto determinado. Para obtener más
información, vea Procedimientos recomendados para excepciones.
En el último ejemplo se muestra cómo controlar los casos en los que se debe producir una excepción durante la
ejecución de una consulta.
Ejemplo 1
En el ejemplo siguiente se muestra cómo mover código de control de excepciones fuera de una expresión de
consulta. Esto solo es posible cuando el método no depende de ninguna variable local de la consulta.
class ExceptionsOutsideQuery
{
static void Main()
{
// DO THIS with a datasource that might
// throw an exception. It is easier to deal with
// outside of the query expression.
IEnumerable<int> dataSource;
try
{
dataSource = GetData();
}
catch (InvalidOperationException)
{
// Handle (or don't handle) the exception
// in the way that is appropriate for your application.
Console.WriteLine("Invalid operation");
goto Exit;
}
Ejemplo 2
En algunos casos, la mejor respuesta a una excepción que se produce dentro de una consulta podría ser detener
la ejecución de la consulta inmediatamente. En el ejemplo siguiente se muestra cómo controlar las excepciones
que pueden producirse desde el cuerpo de una consulta. Supongamos que SomeMethodThatMightThrow puede
producir una excepción que requiere que se detenga la ejecución de la consulta.
Tenga en cuenta que el bloque try encierra el bucle foreach y no la propia consulta. Esto es porque el bucle
foreach es el punto en el que se ejecuta realmente la consulta. Para más información, vea Introducción a las
consultas LINQ.
class QueryThatThrows
{
static void Main()
{
// Data source.
string[] files = { "fileA.txt", "fileB.txt", "fileC.txt" };
Vea también
Language-Integrated Query (LINQ)
Escritura de código C# seguro y eficaz
16/09/2021 • 19 minutes to read
Las nuevas características de C# permiten escribir código seguro verificable con un mejor rendimiento. Si aplica
estas técnicas cuidadosamente, habrá menos escenarios que requieran código no seguro. Estas características
permiten usar con más facilidad las referencias a tipos de valor como argumentos de método y devoluciones de
método. Si aplica estas técnicas de forma segura, minimizará la copia de tipos de valor. Al usar tipos de valor,
también podrá minimizar el número de asignaciones y transferencias de recolección de elementos no utilizados.
En gran parte del código de ejemplo de este artículo se utilizan las características agregadas a la versión 7.2 de
C#. Para usar esas características, asegúrese de que el proyecto no está configurado para usar una versión
anterior. Para obtener más información, vea Configuración de la versión del lenguaje.
Una de las ventajas de utilizar tipos de valor es que a menudo evitan las asignaciones de montón. Pero también
hay una desventaja, que es que se copian por valor. Este último aspecto dificulta la optimización de los
algoritmos que funcionan en grandes cantidades de datos. Las nuevas características del lenguaje proporcionan
mecanismos que permiten escribir código seguro y eficaz mediante referencias a tipos de valor. Use estas
características acertadamente para minimizar tanto las asignaciones como las operaciones de copia.
Algunas de las instrucciones de este artículo se refieren a las prácticas de codificación que siempre son
aconsejables, no solo para la ventaja de rendimiento. Use la palabra clave readonly cuando expresa con
precisión la intención de diseño:
Declare structs inmutables como readonly .
Declare miembros readonly para structs mutables.
En el artículo también se explican algunas optimizaciones de bajo nivel que son aconsejables cuando se ha
ejecutado un generador de perfiles y se han identificado cuellos de botella:
Use el modificador de parámetro in .
Use instrucciones ref readonly return .
Use tipos ref struct .
Use los tipos nint y nuint .
Estas técnicas equilibran dos objetivos contrapuestos:
Minimice las asignaciones en el montón.
Las variables que son tipos de referencia tienen una referencia a una ubicación en memoria y se asignan
en el montón administrado. Solo se copia la referencia cuando un tipo de referencia se pasa como
argumento a un método o se devuelve desde un método. Cada nuevo objeto requiere una nueva
asignación, y se debe reclamar posteriormente. La recolección de elementos no utilizados lleva tiempo.
Minimice la copia de valores.
Las variables que son tipos de valor contienen directamente su valor y el valor normalmente se copia
cuando se pasa a un método o se devuelve desde un método. Este comportamiento incluye copiar el
valor de this al llamar a iteradores y métodos de instancia asincrónica de structs. La operación de copia
tarda tiempo, en función del tamaño del tipo.
En este artículo se utiliza el concepto de ejemplo siguiente de la estructura de punto 3D para explicar estas
recomendaciones:
public struct Point3D
{
public double X;
public double Y;
public double Z;
}
Siga esta recomendación siempre que su intención de diseño sea crear un tipo de valor inmutable. Cualquier
mejora de rendimiento que se aplique es una ventaja adicional. Las palabras clave readonly struct expresan
claramente la intención de diseño.
En el ejemplo anterior se muestran muchas de las ubicaciones en las que puede aplicar el modificador readonly
: métodos, propiedades y descriptores de acceso de propiedad. Si usa propiedades implementadas
automáticamente, el compilador agrega el modificador readonly al descriptor de acceso get para las
propiedades de lectura y escritura. El compilador agrega el modificador readonly a las declaraciones de
propiedad implementadas automáticamente para las propiedades con solo un descriptor de acceso get .
Agregar el modificador readonly a los miembros que no mutan el estado proporciona dos ventajas
relacionadas. En primer lugar, el compilador aplica su intención. Ese miembro no puede mutar el estado de la
estructura. En segundo lugar, el compilador no crea copias defensivas de parámetros in al obtener acceso a un
miembro de readonly . El compilador puede hacer que esta optimización sea segura, ya que garantiza que un
miembro de readonly no modifique struct .
Como no quiere que los autores de llamada modifiquen el origen, debe devolver el valor según su
ref readonly :
Al devolver ref readonly , se podrá ahorrar procesos de copia de estructuras mayores y conservar la
inmutabilidad de los miembros de datos internos.
En el sitio de llamada, los autores de dicha llamada eligen utilizar la propiedad Origin como ref readonly o
como valor:
La primera asignación en el código anterior realiza una copia de la constante Origin y asigna esa copia. La
segunda asigna una referencia. Tenga en cuenta que el modificador readonly debe formar parte de la
declaración de la variable. No se puede modificar la referencia a la que alude. Los intentos de hacerlo generarán
un error en tiempo de compilación.
El modificador readonly es necesario en la declaración de originReference .
El compilador exige que el autor de una llamada no pueda modificar la referencia. Los intentos de asignar el
valor directamente generan un error en tiempo de compilación. En otros casos, el compilador asigna una copia
defensiva a menos que pueda usar sin ningún riesgo la referencia de solo lectura. Las reglas de análisis estático
determinan si se puede modificar la estructura. El compilador no crea una copia defensiva cuando la estructura
es un elemento readonly struct o el miembro es miembro de readonly de la estructura. No se necesitan
copias defensivas para pasar la estructura como un argumento in .
La palabra clave in complementa las palabras clave ref y out para pasar argumentos por referencia. La
palabra clave in especifica que el argumento se pasa por referencia, pero el método al que se llama no
modifica el valor. El modificador in se puede aplicar a cualquier miembro que tome parámetros, como
métodos, delegados, lambdas, funciones locales, indexadores y operadores.
Con la adición de la palabra clave in , C# proporciona un vocabulario completo para expresar la intención de
diseño. Los tipos de valor se copian al pasarlos a un método llamado cuando no se especifica ninguno de los
siguientes modificadores en la firma de método. Cada uno de estos modificadores especifica que una variable
se pasa por referencia, evitando la copia. Cada modificador expresa un propósito diferente:
out : este método establece el valor del argumento utilizado como este parámetro.
ref : este método puede modificar el valor del argumento utilizado como este parámetro.
in : este método no modifica el valor del argumento utilizado como este parámetro.
Agregue el modificador in para pasar un argumento por referencia y declare la intención del diseño de pasar
argumentos por referencia para evitar la copia innecesaria. No pretende modificar el objeto usado como ese
argumento.
El modificador in complementa también a out y ref de otras maneras. No puede crear sobrecargas de un
método que difiere solo en cuanto a la presencia de in , out o ref . Estas nuevas reglas extienden el mismo
comportamiento que siempre se ha definido para los parámetros out y ref . Como en el caso de los
modificadores out y ref , no se aplica la conversión boxing a los tipos de valor porque se aplica el modificador
in . Otra característica de los parámetros in es que puede usar valores literales o constantes para el
argumento en un parámetro in .
El modificador in también se puede usar con tipos de referencia o valores numéricos. Sin embargo, las
ventajas en esos casos son mínimas, si las hay.
El compilador tiene varias maneras de aplicar la naturaleza de solo lectura de un argumento in . En primer
lugar, el método llamado no se puede asignar directamente a un parámetro in . No se puede asignar
directamente a ningún campo de un parámetro in cuando el valor es un tipo struct . Además, no se puede
pasar un parámetro in a ningún método que exija el modificador ref o out . Estas reglas se aplican a
cualquier campo de un parámetro in , siempre que el campo sea un tipo struct y el parámetro también sea
un tipo struct . De hecho, estas reglas se aplican para varios niveles de acceso a miembros, siempre que los
tipos en todos los niveles de acceso a miembros sean structs . El compilador exige que los tipos struct que se
pasan como argumentos in y sus miembros struct sean variables de solo lectura cuando se usan como
argumentos para otros métodos.
Uso de los parámetros in para structs grandes
Puede aplicar el modificador in a cualquier parámetro readonly struct , pero es probable que esta práctica
mejore el rendimiento solo para los tipos de valor que son considerablemente mayores que IntPtr.Size. Para los
tipos simples (como sbyte , byte , short , ushort , int , uint , long , ulong , char , float , double , decimal
y bool , y enum ), las posibles mejoras de rendimiento son mínimas. Algunos tipos simples, como decimal con
un tamaño de 16 bytes, son mayores que las referencias de 4 o 8 bytes, pero no lo suficiente como para marcar
una diferencia medible en el rendimiento en la mayoría de los escenarios. Y el rendimiento puede degradarse
mediante el uso de paso por referencia para tipos menores que IntPtr.Size.
El código siguiente muestra un ejemplo de un método que calcula la distancia entre dos puntos en un espacio
3D.
private static double CalculateDistance(in Point3D point1, in Point3D point2)
{
double xDifference = point1.X - point2.X;
double yDifference = point1.Y - point2.Y;
double zDifference = point1.Z - point2.Z;
Los argumentos son dos estructuras que contienen cada una de ellas tres valores dobles. Un valor doble tiene 8
bytes, por lo que cada argumento es de 24 bytes. Al especificar el modificador in , se pasa una referencia de 4
bytes u 8 bytes a esos argumentos, en función de la arquitectura de la máquina. La diferencia de tamaño es
pequeña, pero puede sumar cuando la aplicación llama a este método en un bucle estrecho mediante muchos
valores diferentes.
Sin embargo, el impacto de las optimizaciones de bajo nivel, como el uso del modificador in , debe medirse
para validar una ventaja de rendimiento. Por ejemplo, podría pensar que usar in en un parámetro Guid sería
beneficioso. El tipo Guid tiene un tamaño de 16 bytes, el doble del tamaño de una referencia de 8 bytes. Sin
embargo, es probable que esta pequeña diferencia no tenga como resultado una ventaja de rendimiento
cuantificable a menos que esté en un método que se encuentra en una ruta de acceso activa crítica en el tiempo
para la aplicación.
Uso opcional de in en el sitio de llamada
A diferencia de un parámetro ref o out , no es necesario aplicar el modificador in en el sitio de llamada. En
el código siguiente se muestran dos ejemplos de llamada al método CalculateDistance . El primero usa dos
variables locales pasadas por referencia. El segundo incluye una variable temporal creada como parte de la
llamada al método.
Al omitir el modificador in en el sitio de llamada, se indica al compilador que está permitido realizar una copia
del argumento por estos motivos:
Hay una conversión implícita, pero no una conversión de identidad desde el tipo de argumento hacia el tipo
de parámetro.
El argumento es una expresión, pero no tiene una variable de almacenamiento conocida.
Existe una sobrecarga que se diferencia por la presencia o la ausencia de in . En ese caso, la sobrecarga por
valor es una coincidencia mejor.
Estas reglas son útiles cuando se actualiza código existente para usar argumentos de referencia de solo lectura.
En el método llamado, puede llamar a cualquier método de instancia que use parámetros por valor. En esos
casos, se crea una copia del parámetro in .
Dado que el compilador puede crear una variable temporal para cualquier parámetro in , también puede
especificar valores predeterminados para cualquier parámetro in . En este código se especifica el origen (punto
0,0,0) como valor predeterminado para el segundo punto:
private static double CalculateDistance2(in Point3D point1, in Point3D point2 = default)
{
double xDifference = point1.X - point2.X;
double yDifference = point1.Y - point2.Y;
double zDifference = point1.Z - point2.Z;
Para forzar al compilador que pase argumentos de solo lectura por referencia, especifique el modificador in en
los argumentos en el sitio de llamada, como se muestra en el este código:
Con este comportamiento es más fácil adoptar parámetros in con el tiempo en grandes bases de código
donde es posible mejorar el rendimiento. Primero, puede agregar el modificador in para las firmas de método.
Después, puede agregar el modificador in en sitios de llamada y crear tipos readonly struct para permitir al
compilador que evite la creación de copias defensivas de parámetros in en más ubicaciones.
Evitar copias defensivas
Pase struct como argumento para un parámetro in solo si se declara con el modificador readonly o si el
método tiene acceso solo a los miembros readonly del struct. De lo contrario, el compilador debe crear copias
defensivas en muchas situaciones para asegurarse de que los argumentos no se mutan. Tenga en cuenta el
comentario siguiente, en el que se calcula la distancia de un punto 3D respecto del origen:
La estructura Point3D no es un valor struct de solo lectura. Hay seis llamadas de acceso a propiedades en el
cuerpo de este método. En el primer examen, es posible que haya pensado que esos accesos son seguros. Al fin
y al cabo, un descriptor de acceso get no debe modificar el estado del objeto. Sin embargo, no hay ninguna
regla de lenguaje que lo exija. Se trata solo de una convención habitual. Cualquier tipo podría implementar un
descriptor de acceso get que haya modificado el estado interno.
Si el compilador no dispone de ninguna garantía relativa al lenguaje, debe crear una copia temporal del
argumento antes de llamar a algún miembro no marcado con el modificador readonly . El almacenamiento
temporal se crea en la pila, los valores del argumento se copian al almacenamiento temporal y el valor se copia
en la pila por cada acceso de miembro como argumento this . En muchas situaciones, estas copias perjudican
tanto el rendimiento que el parámetro de paso por valor es más rápido que el de paso por referencia cuando el
tipo de argumento no es un valor readonly struct , y el método llama a los miembros no marcados con
readonly . Si marca todos los métodos que no modifican el estado de struct como readonly , el compilador
puede determinar con seguridad que no se modifica el estado de struct y no se necesita una copia defensiva.
Si el cálculo de distancia usa la estructura inmutable, ReadonlyPoint3D , no se necesitan objetos temporales:
private static double CalculateDistance3(in ReadonlyPoint3D point1, in ReadonlyPoint3D point2 = default)
{
double xDifference = point1.X - point2.X;
double yDifference = point1.Y - point2.Y;
double zDifference = point1.Z - point2.Z;
El compilador genera un código más eficaz cuando se llama a los miembros de un valor readonly struct . La
referencia this , en lugar de una copia del receptor, es siempre un parámetro in pasado por referencia al
método del miembro. Esta optimización ahorra procesos de copia cuando se utiliza readonly struct como
argumento in .
No pase un tipo de valor que acepta valores NULL como in argumento. El tipo Nullable<T> no se declara
como una estructura de solo lectura. Eso significa que el compilador debe generar copias defensivas de
cualquier argumento de tipo de valor que acepta valores NULL pasado a un método con el modificador in en
la declaración de parámetros.
Puede ver un programa de ejemplo en el que se muestran las diferencias de rendimiento con BenchmarkDotNet
en el repositorio de ejemplos de GitHub. Compara la transmisión de un valor struct mutable por valor y por
referencia con la transmisión de un valor struct inmutable por valor y por referencia. El uso de un valor struct
inmutable y de la transmisión por referencia es un proceso más rápido.
Conclusiones
El uso de tipos de valor minimiza el número de operaciones de asignación:
El almacenamiento de los tipos de valor está asignado a la pila en las variables locales y los argumentos de
método.
En cuanto al almacenamiento de tipos de valor que son miembros de otros objetos, está asignado como
parte del objeto en cuestión, no como una asignación separada.
Respecto a los valores devueltos por tipo de valor, están asignados a la pila.
Es necesario contrastar eso con los tipos de referencia en estas mismas situaciones:
El almacenamiento de los tipos de referencia se asigna al montón en las variables locales y los argumentos
de método. La referencia se almacena en la pila.
El almacenamiento de tipos de referencia que son miembros de otros objetos está asignado por separado en
la pila. El objeto contenedor almacena la referencia.
El almacenamiento de los valores devueltos por tipo de referencia está asignado a la pila. La referencia a ese
almacenamiento se guarda en la pila.
La minimización de asignaciones conlleva una serie de renuncias. Se copia más memoria cuando el tamaño del
valor struct es mayor que el de una referencia. Habitualmente, las referencias son de 64 o 32 bits y dependen
de la CPU de la máquina de destino.
Dichas renuncias suelen tener un impacto mínimo en el rendimiento. Sin embargo, en el caso de los valores
struct grandes o de las colecciones de mayor tamaño, el impacto sobre el rendimiento es mayor. Dicho impacto
puede ser considerable en los bucles estrechos y las rutas de acceso activas de los programas.
Estas mejoras del lenguaje C# están diseñadas para algoritmos en los que el rendimiento es fundamental y la
minimización de asignaciones de memoria es un factor principal para lograr el rendimiento necesario. Es
posible que no use a menudo estas características en el código que escribe. Sin embargo, estas mejoras se han
adoptado a través de .NET. Dado que cada vez son más las API que utilizan estas características, verá que el
rendimiento de sus aplicaciones aumenta.
Vea también
Modificador del parámetro in (referencia de C#)
ref (palabra clave)
Devoluciones y variables locales ref
Árboles de expresión
16/09/2021 • 2 minutes to read
Si ha usado LINQ, tiene experiencia con una extensa biblioteca donde los tipos Func forman parte del conjunto
de API. (Si no está familiarizado con LINQ, probablemente quiera leer el tutorial de LINQ y el artículo sobre
expresiones lambda antes de este). Los árboles de expresión proporcionan una interacción más amplia con los
argumentos que son funciones.
Escribe argumentos de función, normalmente con expresiones lambda, cuando crea consultas LINQ. En una
consulta LINQ habitual, esos argumentos de función se transforman en un delegado que crea el compilador.
Cuando quiera tener una interacción mayor, necesita usar los árboles de expresión. Los árboles de expresión
representan el código como una estructura que puede examinar, modificar o ejecutar. Estas herramientas le
proporcionan la capacidad de manipular el código durante el tiempo de ejecución. Puede escribir código que
examine la ejecución de algoritmos o inserte nuevas características. En escenarios más avanzados, puede
modificar la ejecución de algoritmos e incluso convertir expresiones de C# en otra forma para su ejecución en
otro entorno.
Probablemente ya ha escrito código que use árboles de expresión. Las API de LINQ de Entity Framework
admiten árboles de expresión como argumentos para el patrón de expresión de consulta LINQ. Eso permite que
Entity Framework convierta la consulta que ha escrito en C# en SQL que se ejecuta en el motor de base de
datos. Otro ejemplo es Moq, que es un marco simulado popular de .NET.
En las secciones restantes de este tutorial exploraremos lo que son los árboles de expresión, examinaremos las
clases de marco que admiten los árboles de expresión y le mostraremos cómo trabajar con ellos. Obtendrá
información sobre cómo leer árboles de expresión, cómo crearlos, cómo crear árboles de expresión modificados
y cómo ejecutar el código representado en ellos. Después de esta lectura, estará listo para usar estas estructuras
para crear algoritmos muy adaptables.
1. Árboles de expresiones en detalle
Comprenda la estructura y los conceptos de los árboles de expresión.
2. Tipos de marco que admiten árboles de expresión
Obtenga información sobre las estructuras y las clases que definen y manipulan los árboles de expresión.
3. Ejecución de expresiones
Obtenga información sobre cómo convertir un árbol de expresión representado como una expresión
lambda en un delegado y ejecutar el delegado resultante.
4. Interpretación de expresiones
Obtenga información sobre cómo recorrer y examinar árboles de expresión para entender qué código
representa el árbol de expresión.
5. Generación de expresiones
Obtenga información sobre cómo construir los nodos de un árbol de expresión y crear árboles de
expresión.
6. Traducción de expresiones
Obtenga información sobre cómo crear una copia modificada de un árbol de expresión, o convertir un
árbol de expresión en un formato diferente.
7. Resumen
Revise la información sobre los árboles de expresión.
Árboles de expresiones en detalle
16/09/2021 • 5 minutes to read
var sum = 1 + 2;
Si fuera a analizarlo como un árbol de expresión, el árbol contiene varios nodos. El nodo más externo es una
instrucción de declaración de variable con asignación ( var sum = 1 + 2; ). Ese nodo exterior contiene varios
nodos secundarios: una declaración de variable, un operador de asignación y una expresión que representa el
lado derecho del signo igual. Esa expresión se subdivide aún más en expresiones que representan la operación
de suma, y los operandos izquierdo y derecho de la suma.
Vamos a profundizar un poco más en las expresiones que constituyen el lado derecho del signo igual. La
expresión es 1 + 2 . Se trata de una expresión binaria. Concretamente, es una expresión binaria de suma. Una
expresión binaria de suma tiene dos elementos secundarios, que representan los nodos izquierdo y derecho de
la expresión de suma. Aquí, ambos nodos son expresiones constantes: El operador izquierdo es el valor 1 y el
operador derecho, el valor 2 .
Visualmente, toda la instrucción es un árbol: puede empezar en el nodo raíz y desplazarse a cada uno de los
nodos del árbol para ver el código que compone la instrucción:
Instrucción de declaración de variable con asignación ( var sum = 1 + 2; )
Declaración de tipo de variable implícita ( var sum )
Palabra clave var implícita ( var )
Declaración de nombre de variable ( sum )
Operador de asignación ( = )
Expresión binaria de suma ( 1 + 2 )
Operando izquierdo ( 1 )
Operador de suma ( + )
Operando derecho ( 2 )
Esto puede parecer complicado, pero resulta muy eficaz. Siguiendo el mismo proceso, puede descomponer
expresiones mucho más complicadas. Tomemos esta expresión como ejemplo:
var finalAnswer = this.SecretSauceFunction(
currentState.createInterimResult(), currentState.createSecondValue(1, 2),
decisionServer.considerFinalOptions("hello")) +
MoreSecretSauce('A', DateTime.Now, true);
La expresión anterior también es una declaración de variable con una asignación. En este caso, el lado derecho
de la asignación es un árbol mucho más complicado. No voy a descomponer esta expresión, pero tenga en
cuenta lo que podrían ser los distintos nodos. Hay llamadas de método que usan el objeto actual como un
receptor, una que tiene un receptor this explícito y otra que no. Hay llamadas de método que usan otros
objetos de receptor, así como argumentos constantes de tipos diferentes. Y, por último, hay un operador binario
de suma. Según el tipo de valor devuelto de SecretSauceFunction() o MoreSecretSauce() , ese operador binario
de suma puede ser una llamada de método a un operador de suma invalidado, que se resuelva en una llamada
de método estático al operador binario de suma definido para una clase.
A pesar de esta aparente complejidad, la expresión anterior crea una estructura de árbol por la que se puede
navegar con tanta facilidad como en el primer ejemplo. Puede seguir recorriendo los nodos secundarios para
buscar nodos hoja en la expresión. Los nodos primarios tendrán referencias a sus elementos secundarios, y cada
nodo tiene una propiedad que describe de qué tipo es.
La estructura de los árboles de expresiones es muy coherente. Una vez que conozca los aspectos básicos, podrá
entender incluso el código más complejo cuando esté representado como un árbol de expresión. La elegancia
de la estructura de datos explica cómo el compilador de C# puede analizar los programas de C# más complejos
y crear resultados correctos a partir de código fuente complicado.
Una vez que esté familiarizado con la estructura de los árboles de expresiones, verá que los conocimientos que
ha adquirido le permiten trabajar rápidamente con muchos escenarios más avanzados. Los árboles de
expresiones ofrecen posibilidades increíbles.
Además de traducir algoritmos para ejecutarlos en otros entornos, se pueden usar árboles de expresiones para
que resulte más fácil escribir algoritmos que inspeccionen el código antes de ejecutarlo. Puede escribir un
método cuyos argumentos sean expresiones y, luego, examinar esas expresiones antes de ejecutar el código. El
árbol de expresión es una representación completa del código: puede ver los valores de cualquier subexpresión.
Puede ver los nombres de propiedad y método. Puede ver el valor de las expresiones constantes. También
puede convertir un árbol de expresión en un delegado ejecutable y ejecutar el código.
Las API de los árboles de expresiones permiten crear árboles que representan casi cualquier construcción de
código válida. En cambio, para que todo resulte lo más sencillo posible, algunas expresiones de C# no se pueden
crear en un árbol de expresión. Un ejemplo son las expresiones asincrónicas (mediante las palabras clave async
y await ). Si necesita algoritmos asincrónicos, tendría que manipular los objetos Task directamente, en lugar
de confiar en la compatibilidad del compilador. Otro ejemplo es en la creación de bucles. Normalmente, puede
crearlos usando bucles for , foreach , while o do . Como verá más adelante en esta serie, las API de los
árboles de expresiones admiten una expresión de bucle individual, con expresiones break y continue que
controlan la repetición del bucle.
Lo único lo que no se puede hacer es modificar un árbol de expresión. Los árboles de expresiones son
estructuras de datos inmutables. Si quiere mutar (cambiar) un árbol de expresión, debe crear un nuevo árbol
que sea una copia del original, pero con los cambios que quiera.
Siguiente: Tipos de marco que admiten árboles de expresión
Tipos de marco que admiten árboles de expresión
16/09/2021 • 3 minutes to read
if (addFive.NodeType == ExpressionType.Lambda)
{
var lambdaExp = (LambdaExpression)addFive;
Console.WriteLine(parameter.Name);
Console.WriteLine(parameter.Type);
}
En este sencillo ejemplo puede ver que hay muchos tipos implicados a la hora de crear árboles de expresiones y
trabajar con ellos. Esta complejidad resulta necesaria para proporcionar las capacidades del vocabulario variado
que ofrece el lenguaje C#.
Encontrará más información a medida que observe cada una de esas tres áreas. Siempre encontrará lo que
necesita empezando con uno de esos tres pasos.
Siguiente: Ejecutar árboles de expresión
Ejecución de árboles de expresión
16/09/2021 • 6 minutes to read
Observe que el tipo de delegado se basa en el tipo de expresión. Debe conocer el tipo de valor devuelto y la lista
de argumentos si quiere usar el objeto de delegado de una forma fuertemente tipada. El método
LambdaExpression.Compile() devuelve el tipo Delegate . Tendrá que convertirlo al tipo de delegado correcto para
que las herramientas de tiempo de compilación comprueben la lista de argumentos del tipo de valor devuelto.
Ejecución y duraciones
El código se ejecuta mediante la invocación del delegado que se crea al llamar a LambdaExpression.Compile() .
Puede verlo encima de donde add.Compile() devuelve un delegado. Invocar ese delegado, mediante una
llamada a func() ejecuta el código.
Ese delegado representa el código en el árbol de expresión. Se puede conservar el identificador a ese delegado e
invocarlo más adelante. No es necesario compilar el árbol de expresión cada vez que se quiera ejecutar el
código que representa. (Recuerde que los árboles de expresión son inmutables y que compilar el mismo árbol
de expresión más adelante creará un delegado que ejecuta el mismo código).
No es aconsejable intentar crear cualquier mecanismo de almacenamiento en caché más sofisticado para
aumentar el rendimiento evitando llamadas innecesarias de compilación. La comparación de dos árboles de
expresión arbitrarios para determinar si representan el mismo algoritmo también consumirá mucho tiempo de
ejecución. Probablemente comprobará que el tiempo de proceso que se ahorra al evitar llamadas adicionales a
LambdaExpression.Compile() será consumido por el tiempo de ejecución de código que determina que dos
árboles de expresión diferentes devuelven el mismo código ejecutable.
Advertencias
Compilar una expresión lambda en un delegado e invocar ese delegado es una de las operaciones más simples
que se pueden realizar con un árbol de expresión. Pero incluso con esta sencilla operación, hay advertencias que
debe conocer.
Las expresiones lambda crean clausuras sobre las variables locales a las que se hace referencia en la expresión.
Debe garantizar que las variables que formarían parte del delegado se pueden usar en la ubicación desde la que
se llama a Compile , y cuando se ejecuta el delegado resultante.
En general, el compilador se asegurará de que esto es cierto. Pero si la expresión tiene acceso a una variable que
implementa IDisposable , es posible que el código deseche el objeto mientras se sigue manteniendo en el árbol
de expresión.
Por ejemplo, este código funciona bien porque int no implementa IDisposable :
El delegado capturó una referencia a la variable local constant . Esa variable es accesible en cualquier momento
posterior, cuando se ejecuta la función devuelta por CreateBoundFunc .
Pero considere esta clase (bastante artificiosa) que implementa IDisposable :
public class Resource : IDisposable
{
private bool isDisposed = false;
public int Argument
{
get
{
if (!isDisposed)
return 5;
else throw new ObjectDisposedException("Resource");
}
}
Si se usa en una expresión como se muestra a continuación, obtendrá una ObjectDisposedException al ejecutar
el código al que hace referencia la propiedad Resource.Argument :
El delegado devuelto por este método se clausuró sobre el objeto constant , que se eliminó. (Se eliminó porque
se declaró en una instrucción using ).
Ahora, al ejecutar el delegado devuelto por este método, se producirá una excepción ObjectDisposedException
en el punto de ejecución.
Parece extraño tener un error en tiempo de ejecución que representa una construcción de tiempo de
compilación, pero es el mundo al que se entra cuando se trabaja con árboles de expresión.
Hay una gran cantidad de permutaciones de este problema, por lo que resulta difícil ofrecer instrucciones
generales para evitarlo. Tenga cuidado al obtener acceso a las variables locales al definir expresiones y al
obtener acceso al estado en el objeto actual (representado por this ) al crear un árbol de expresión que pueda
ser devuelto por una API pública.
El código de la expresión puede hacer referencia a métodos o propiedades de otros ensamblados. Ese
ensamblado debe ser accesible cuando se define la expresión, cuando se compila y cuando se invoca el
delegado resultante. En los casos en los que no esté presente, se producirá una excepción
ReferencedAssemblyNotFoundException .
Resumen
Los árboles de expresión que representan expresiones lambda se pueden compilar para crear un delegado que
se puede ejecutar. Esto proporciona un mecanismo para ejecutar el código representado por un árbol de
expresión.
El árbol de expresión representa el código que se ejecutaría para cualquier construcción que se cree. Mientras
que el entorno donde se compile y ejecute el código coincida con el entorno donde se crea la expresión, todo
funciona según lo esperado. Cuando eso no sucede, los errores son muy predecibles y se detectarán en las
primeras pruebas de cualquier código que use los árboles de expresión.
Siguiente: Interpretación de expresiones
Interpretación de expresiones
16/09/2021 • 14 minutes to read
Ahora, vamos a escribir el código que examinará esta expresión y también algunas propiedades importantes
sobre este. Aquí se muestra el código:
No estoy usando var para declarar este árbol de expresión, y no es posible porque el lado derecho de la
asignación tiene un tipo implícito.
El nodo raíz es LambdaExpression . Para obtener el código que nos interesa en el lado derecho del operador => ,
hay que buscar uno de los elementos secundarios de LambdaExpression . Haremos esto con todas las
expresiones de esta sección. El nodo primario nos ayuda a encontrar el tipo de valor devuelto de
LambdaExpression .
Para examinar cada nodo de esta expresión, necesitaremos visitar recursivamente varios nodos. Aquí se muestra
una primera implementación sencilla:
Observará muchas repeticiones en el ejemplo de código anterior. Vamos a limpiarlo y a crear un visitante del
nodo de expresión con una finalidad más general. Para ello vamos a necesitar escribir un algoritmo recursivo.
Cualquier nodo puede ser de un tipo que pueda tener elementos secundarios. Cualquier nodo que tenga
elementos secundarios necesita que los visitemos y determinemos cuál es ese nodo. Aquí se muestra una
versión limpia que usa la recursividad para visitar las operaciones de adición:
// Lambda Visitor
public class LambdaVisitor : Visitor
{
private readonly LambdaExpression node;
public LambdaVisitor(LambdaExpression node) : base(node)
{
this.node = node;
}
// Parameter visitor:
public class ParameterVisitor : Visitor
{
private readonly ParameterExpression node;
public ParameterVisitor(ParameterExpression node) : base(node)
{
this.node = node;
this.node = node;
}
// Constant visitor:
public class ConstantVisitor : Visitor
{
private readonly ConstantExpression node;
public ConstantVisitor(ConstantExpression node) : base(node)
{
this.node = node;
}
Este algoritmo es la base de un algoritmo que puede visitar cualquier LambdaExpression arbitrario. Hay muchas
vulnerabilidades, concretamente que el código que he creado solo busca una muestra muy pequeña de los
posibles conjuntos de nodos de árbol de expresión que puede encontrar. En cambio, todavía puede aprender
bastante de lo que genera. (El caso predeterminado en el método Visitor.CreateFromExpression imprime un
mensaje a la consola de error cuando se detecta un nuevo tipo de nodo. De esta manera, sabe que se va a
agregar un nuevo tipo de expresión).
Cuando ejecuta este visitante en la expresión de adición que se muestra arriba, obtiene el siguiente resultado:
Ahora que ha creado una implementación de visitante más general, puede visitar y procesar muchos más tipos
de expresiones diferentes.
Puede ver la separación en dos respuestas posibles para resaltar la más prometedora. La primera representa las
expresiones asociativas por la derecha . La segunda representa las expresiones asociativas por la izquierda . La
ventaja de los dos formatos es que el formato escala a cualquier número arbitrario de expresiones de adición.
Si ejecuta esta expresión a través del visitante, verá este resultado y comprobará que la expresión de adición
simple es asociativa por la izquierda .
Para ejecutar este ejemplo, y ver el árbol de expresión completo, tuve que realizar un cambio en el árbol de
expresión de origen. Cuando el árbol de expresión contiene todas las constantes, el árbol resultante
simplemente contiene el valor constante de 10 . El compilador realiza toda la adición y reduce la expresión a su
forma más simple. Simplemente con agregar una variable a la expresión es suficiente para ver el árbol original:
Cree un visitante para esta suma y ejecute el visitante para ver este resultado:
También puede ejecutar cualquiera de los otros ejemplos a través del código de visitante y ver qué árbol
representa. Aquí se muestra un ejemplo de la expresión sum3 anterior (con un parámetro adicional para evitar
que el compilador calcule la constante):
Expression<Func<int, int, int>> sum3 = (a, b) => (1 + a) + (3 + b);
Tenga en cuenta que los paréntesis no forman parte del resultado. No existen nodos en el árbol de expresión
que representen los paréntesis en la expresión de entrada. La estructura del árbol de expresión contiene toda la
información necesaria para comunicar la precedencia.
Este código representa una posible implementación para la función factorial matemática. La manera en que he
escrito este código resalta dos limitaciones en la creación de árboles de expresión asignando expresiones
lambda a las expresiones. En primer lugar, las expresiones lambda de instrucción no están permitidas. Eso
significa que no puedo usar bucles, bloques, instrucciones IF/ELSE ni otras estructuras de control comunes en
C#. Estoy limitado al uso de expresiones. En segundo lugar, no puedo llamar recursivamente a la misma
expresión. Podría si ya fuera un delegado, pero no puedo llamarla en su forma de árbol de expresión. En la
sección sobre la creación de árboles de expresión, obtendrá las técnicas para superar estas limitaciones.
En esta expresión, encontrará nodos de todos estos tipos:
1. Equal (expresión binaria)
2. Multiply (expresión binaria)
3. Conditional (la expresión ? :)
4. Expresión de llamada a método (con la llamada a Range() y Aggregate() )
Una manera de modificar el algoritmo de visitante es seguir ejecutándolo y escribir el tipo de nodo cada vez que
llegue a su cláusula default . Después de varias iteraciones, habrá visto cada uno de los nodos potenciales.
Después, tiene todo lo que necesita. El resultado será similar al siguiente:
Crear nodos
Comencemos por algo relativamente sencillo de nuevo. Usaremos la expresión de adición con la que he estado
trabajando en estas secciones:
Para crear ese árbol de expresión, debe crear los nodos de hoja. Los nodos de hoja son constantes, por lo que
puede usar el método Expression.Constant para crear los nodos:
Una vez que tenga la expresión de adición, puede crear la expresión lambda:
Esta es una expresión lambda muy sencilla porque no contiene argumentos. Posteriormente en esta sección,
verá cómo asignar argumentos a parámetros y crear expresiones más complicadas.
Para las expresiones que son tan simples como esta, puede combinar todas las llamadas en una sola instrucción:
Después, necesita crear una expresión de llamada al método para la llamada a Math.Sqrt .
Y, por último, coloque la llamada al método en una expresión lambda y asegúrese de definir los argumentos en
dicha expresión:
En este ejemplo más complejo, verá un par de técnicas más que necesitará a menudo para crear árboles de
expresión.
Primero, necesita crear los objetos que representan parámetros o variables locales antes de usarlos. Una vez
que haya creado esos objetos, puede usarlos en su árbol de expresión siempre que los necesite.
Después, necesita usar un subconjunto de las API de reflexión para crear un objeto MethodInfo , de manera que
pueda crear un árbol de expresión para tener acceso a ese método. Debe limitarse al subconjunto de las API de
reflexión que están disponibles en la plataforma de .NET Core. De nuevo, estas técnicas se extenderán a otros
árboles de expresión.
Observe anteriormente que no he creado el árbol de expresión, sino simplemente el delegado. Con la clase
Expression no puede crear expresiones lambda de instrucción. Aquí se muestra el código necesario para crear
la misma función. Es complicado por el hecho de que no existe una API para crear un bucle while , en su lugar
necesita crear un bucle que contenga una prueba condicional y un destino de la etiqueta para salir del bucle.
El código para crear el árbol de expresión para la función factorial es bastante más largo, más complicado y está
lleno de etiquetas, instrucciones Break y otros elementos que nos gustaría evitar en nuestras tareas de
codificación diarias.
En esta sección, también he actualizado el código del visitante para visitar cada nodo de este árbol de expresión
y escribir información sobre los nodos que se crean en este ejemplo. Puede ver o descargar el código de
ejemplo en el repositorio dotnet/docs de GitHub. Pruébelo compilando y ejecutando los ejemplos. Para obtener
instrucciones de descarga, vea Ejemplos y tutoriales.
Trasladar es visitar
El código que se compila para trasladar un árbol de expresión es una extensión de lo que ya se vio para visitar
todos los nodos de un árbol. Al trasladar un árbol de expresión, se visitan todos los nodos y mientras se visitan,
se crea el árbol nuevo. El nuevo árbol puede contener referencias a los nodos originales o a nodos nuevos que
se colocaron en el árbol.
Vamos a verlo en acción mediante la visita a un árbol de expresión y la creación de un árbol nuevo con varios
nodos de reemplazo. En este ejemplo, se van a sustituir todas las constantes con una constante que es diez
veces mayor. De lo contrario, el árbol de expresión se mantendrá intacto. En lugar de leer el valor de la constante
y reemplazarlo con una constante nueva, este cambio se hará reemplazando el nodo constante con un nuevo
nodo que realiza la multiplicación.
Aquí, una vez que se encuentre un nodo constante, se crea un nuevo nodo de multiplicación cuyos elementos
secundarios son la constante original y la constante 10 :
Al reemplazar el nodo original con el sustituto, se crea un árbol nuevo que contiene las modificaciones. Se
puede comprobar mediante la compilación y ejecución del árbol reemplazado.
var one = Expression.Constant(1, typeof(int));
var two = Expression.Constant(2, typeof(int));
var addition = Expression.Add(one, two);
var sum = ReplaceNodes(addition);
var executableFunc = Expression.Lambda(sum);
La creación de un árbol nuevo es una combinación de visitar los nodos del árbol existente y crear nodos nuevos
e insertarlos en el árbol.
En este ejemplo se muestra la importancia de la inmutabilidad de los árboles de expresión. Observe que el
nuevo árbol creado anteriormente contiene una mezcla de los nodos recién creados y los nodos del árbol
existente. Es seguro, ya que no se pueden modificar los nodos en el árbol existente. Esto puede producir
eficiencias de memoria significativas. Los mismos nodos se pueden usar en un árbol o en varios árboles de
expresión. Dado que los nodos no se pueden modificar, se puede volver a usar el mismo nodo siempre que sea
necesario.
Aquí hay gran cantidad de código, pero los conceptos son muy accesibles. Este código visita los elementos
secundarios en una primera búsqueda de profundidad. Cuando encuentra un nodo constante, el visitante
devuelve el valor de la constante. Tras la visita a los dos elementos secundarios por parte del visitante, ambos
elementos habrán obtenido la suma calculada para ese subárbol. El nodo de adición ahora puede calcular la
suma. Una vez que se visiten todos los nodos en el árbol de expresión, se habrá calculado la suma. Se puede
hacer el seguimiento de la ejecución ejecutando el ejemplo en el depurador y realizando el seguimiento de la
ejecución.
Vamos a facilitar el seguimiento de cómo se analizan los nodos y cómo se calcula la suma mediante el recorrido
del árbol. Esta es una versión actualizada del método agregado que incluye gran cantidad de información de
seguimiento:
10
Found Addition Expression
Computing Left node
Found Addition Expression
Computing Left node
Found Constant: 1
Left is: 1
Computing Right node
Found Constant: 2
Right is: 2
Computed sum: 3
Left is: 3
Computing Right node
Found Addition Expression
Computing Left node
Found Constant: 3
Left is: 3
Computing Right node
Found Constant: 4
Right is: 4
Computed sum: 7
Right is: 7
Computed sum: 10
10
Realice el seguimiento del resultado y siga el código anterior. Debería poder averiguar cómo el código visita
cada nodo y calcula la suma mientras recorre el árbol y busca la suma.
Ahora, veremos una ejecución diferente, con la expresión proporcionada por sum1 :
Aunque la respuesta final es la misma, el recorrido del árbol es completamente diferente. Los nodos se recorren
en un orden diferente, porque el árbol se construyó con diferentes operaciones que se producen en primer
lugar.
Limitaciones
Hay algunos elementos de lenguaje de C# nuevos que no se traducen correctamente en árboles de expresión.
Los árboles de expresión no pueden contener expresiones await ni expresiones lambda async . Muchas de las
características agregadas en la versión 6 de C# no aparecen tal y como se escriben en árboles de expresión. En
su lugar, se expondrán las características más recientes de los árboles de expresión en la sintaxis equivalente
anterior. Es posible que esta limitación no sea tanta como podría parecer. De hecho, significa que el código que
interpreta los árboles de expresión probablemente funcionará igual cuando se introduzcan las nuevas
características de lenguaje.
Incluso con estas limitaciones, los árboles de expresión permiten crear algoritmos dinámicos que se basan en la
interpretación y la modificación del código que se representa como una estructura de datos. Es una herramienta
eficaz y es una de las características del ecosistema .NET que permite que las bibliotecas enriquecidas como
Entity Framework realicen lo que hacen.
Interoperabilidad (Guía de programación de C#)
16/09/2021 • 2 minutes to read
En esta sección
Información general sobre interoperabilidad
Se describen métodos para habilitar la interoperabilidad entre el código administrado y el código no
administrado de C#.
Procedimiento para acceder a objetos de interoperabilidad de Office mediante características de C#
Describe las características introducidas en Visual C# para facilitar la programación de Office.
Procedimiento para usar propiedades indizadas en la programación de interoperabilidad COM
Se describe cómo utilizar las propiedades indizadas para acceder a las propiedades de COM que tienen
parámetros.
Procedimiento para usar la invocación de plataforma para reproducir un archivo WAV
Se describe cómo usar los servicios de invocación de plataforma para reproducir un archivo de sonido .wav en
el sistema operativo Windows.
Tutorial: Programación de Office
Muestra cómo crear un libro de Excel y un documento de Word que contiene un vínculo al libro.
Clase COM de ejemplo
Muestra cómo exponer una clase de C# como un objeto COM.
Vea también
Marshal.ReleaseComObject
Guía de programación de C#
Interoperar con código no administrado
Tutorial: Programación de Office
Control de versiones en C#
16/09/2021 • 6 minutes to read
En este tutorial, obtendrá información sobre qué significa el control de versiones en .NET. También obtendrá
información sobre los factores que deben tenerse en cuenta para controlar las versiones de su biblioteca así
como para actualizar a una versión nueva de esta.
Creación de bibliotecas
Como desarrollador que ha creado bibliotecas de .NET para uso público, probablemente se ha encontrado en
situaciones en las que tiene que implementar nuevas actualizaciones. Cómo realizar este proceso es muy
importante, ya que necesita asegurarse de que existe una transición sin problemas del código existente a la
versión nueva de su biblioteca. Aquí se muestran algunos aspectos para tener en cuenta a la hora de crear una
versión nueva:
Control de versiones semántico
Control de versiones semántico (SemVer para abreviar) es una convención de nomenclatura que se aplica a las
versiones de su biblioteca para indicar eventos importantes específicos. De manera ideal, la información de la
versión que proporciona a la biblioteca debe ayudar a los desarrolladores a determinar la compatibilidad con
sus proyectos que usan versiones anteriores de la misma biblioteca.
El enfoque más sencillo de SemVer es el formato de 3 componentes MAJOR.MINOR.PATCH , donde:
MAJOR se incrementa cuando realiza cambios de API incompatibles
MINOR se incrementa cuando agrega funciones de manera compatible con versiones anteriores
PATCH se incrementa cuando realiza correcciones de errores compatibles con versiones anteriores
También existen maneras de especificar otros escenarios como versiones preliminares, etc. al aplicar
información de la versión a su biblioteca .NET.
Compatibilidad con versiones anteriores
A medida que presente versiones nuevas de su biblioteca, la compatibilidad con las versiones anteriores será
probablemente una de sus mayores preocupaciones. Una versión nueva de su biblioteca es compatible en su
origen con una versión anterior si el código que depende de la versión anterior, puede, cuando se vuelve a
compilar, trabajar con la versión nueva. Una versión nueva de su biblioteca tiene compatibilidad binaria si una
aplicación que dependía de la versión anterior, puede, sin que se vuelva a compilar, trabajar con la versión
nueva.
Aquí se muestran algunos aspectos a tener en cuenta al intentar mantener la compatibilidad con versiones
anteriores de su biblioteca:
Métodos virtuales: cuando hace que un método virtual sea no virtual en la versión nueva, significa que los
proyectos que reemplacen ese método tendrán que actualizarse. Esto es un cambio brusco enorme y se
desaconseja totalmente.
Firmas de método: cuando actualizar un comportamiento del método requiere que también se cambie su
firma, en su lugar se debe crear una sobrecarga de manera que el código que llama a ese método siga
funcionando. Siempre puede manipular la firma del método anterior para llamar a la firma del método
nuevo, de manera que la implementación siga siendo coherente.
Atributo obsoleto: puede usar este atributo en el código para especificar clases o miembros de clases que
han quedado obsoletos y que probablemente se quiten en versiones futuras. Esto garantiza que los
desarrolladores que usen su biblioteca estén mejor preparados para los cambios bruscos.
Argumentos de método opcionales: cuando hace que los argumentos de método opcionales anteriores sean
obligatorios o cambien su valor predeterminado, se tendrá que actualizar todo el código que no proporcione
esos argumentos.
NOTE
Hacer que los argumentos obligatorios sean opcionales debe tener un efecto muy pequeño, especialmente si no cambia el
comportamiento del método.
Cuánto más facilite la actualización a la nueva versión de la biblioteca a sus usuarios, más rápidamente la
actualizarán.
Archivo de configuración de aplicación
Como desarrollador de .NET, existe una posibilidad muy alta de que haya encontrado el archivo app.config en
la mayoría de tipos de proyecto. Este sencillo archivo de configuración puede hacer mucho por mejorar la
implementación de las actualizaciones nuevas. Generalmente, debe diseñar sus bibliotecas de tal manera que la
información que es probable que cambie regularmente se almacene en el archivo app.config , de esta manera,
cuando dicha información se actualice, el archivo de configuración de las versiones anteriores solo necesita
reemplazarse por el nuevo sin necesidad de volver a compilar la biblioteca.
Consumo de bibliotecas
Como desarrollador que consume bibliotecas .NET creadas por otros desarrolladores, es probable que sea
consciente de que una nueva versión de una biblioteca puede que no sea completamente compatible con su
proyecto y, a menudo, puede que tenga que actualizar su código para trabajar con esos cambios.
Por suerte, C# y el ecosistema de .NET incluyen características y técnicas que nos permiten actualizar fácilmente
nuestra aplicación para que funcione con las versiones nuevas de bibliotecas que pueden presentar cambios
bruscos.
Redirección de enlace de ensamblados
Puede usar el archivo app.config para actualizar la versión de una biblioteca que use su aplicación. Al agregar lo
que se denomina una redirección de enlace, se puede usar la nueva versión de la biblioteca sin tener que volver
a compilar la aplicación. En el siguiente ejemplo se muestra cómo actualizaría el archivo app.config de la
aplicación para usar la versión de revisión 1.0.1 de ReferencedLibrary , en lugar de la versión 1.0.0 con la
que se ha compilado originalmente.
<dependentAssembly>
<assemblyIdentity name="ReferencedLibrary" publicKeyToken="32ab4ba45e0a69a1" culture="en-us" />
<bindingRedirect oldVersion="1.0.0" newVersion="1.0.1" />
</dependentAssembly>
NOTE
Este enfoque solo funcionará si la versión nueva de ReferencedLibrary tiene compatibilidad binaria con su aplicación.
Vea la sección anterior Compatibilidad con versiones anteriores para ver los cambios que debe tener en cuenta al
determinar la compatibilidad.
new
Use el modificador new para ocultar los miembros heredados de una clase base. Esta es una manera en la que
las clases derivadas pueden responder a las actualizaciones en clases base.
Considere el ejemplo siguiente:
public class BaseClass
{
public void MyMethod()
{
Console.WriteLine("A base method");
}
}
b.MyMethod();
d.MyMethod();
}
Salida
A base method
A derived method
En el ejemplo anterior puede ver cómo DerivedClass oculta el método MyMethod presente en BaseClass . Esto
significa que cuando una clase base en la versión nueva de una biblioteca agrega un miembro que ya existe en
su clase derivada, simplemente puede usar el modificador new en su miembro de clase derivada para ocultar el
miembro de clase base.
Cuando no se especifica ningún modificador new , una clase derivada ocultará de manera predeterminada los
miembros en conflicto de una clase base, aunque se generará una advertencia del compilador, el código se
compilará. Esto significa que agregar simplemente miembros nuevos a una clase existente, hace que la versión
nueva de su biblioteca tenga compatibilidad binaria y de origen con el código que depende de ella.
override
El modificador override significa que una implementación derivada extiende la implementación de un
miembro de clase base en lugar de ocultarlo. El miembro de clase base necesita que se le aplique el modificador
virtual .
public class MyBaseClass
{
public virtual string MethodOne()
{
return "Method One";
}
}
Salida
Conceptos generales de C#
Hay varios trucos y sugerencias que son habituales entre los desarrolladores de C#:
Inicialice los objetos usando un inicializador de objeto.
Obtenga información sobre las diferencias entre pasar una estructura o una clase a un método.
Use la sobrecarga de operadores.
Implemente e invoque un método de extensión personalizado.
Incluso los programadores de C# es posible que quieran usar el espacio de nombres My de Visual Basic.
Cree un nuevo método para un tipo enum mediante métodos de extensión.
Miembros de clase, registro y estructura
Para implementar un programa se usan clases, registros y estructuras. Estas técnicas suelen usarse al escribir
clases, registros o estructuras.
Declare propiedades de implementación automática.
Declare y use propiedades de lectura y escritura.
Defina las constantes.
Invalide el método ToString para proporcionar la salida de la cadena.
Defina las propiedades abstractas.
Use las características de documentación XML para documentar el código.
Implemente explícitamente miembros de interfaz para que su interfaz pública sea concisa.
Implemente explícitamente miembros de dos interfaces.
Trabajar con colecciones
Estos artículos le ayudarán a trabajar con colecciones de datos.
Inicialice un diccionario con un inicializador de colección.
Control de excepciones
Los programas de .NET informan de que un método no se ha ejecutado correctamente y de que se han
generado excepciones. En estos artículos aprenderá a trabajar con excepciones.
Controle excepciones mediante try y catch .
Limpie recursos con cláusulas finally .
Recupere excepciones no conformes a CLS (Common Language Specification).
Delegados y eventos
Los delegados y los eventos proporcionan capacidad para las estrategias que implican bloques de código sin
una conexión directa.
Declare y use delegados, y cree instancias de estos.
Combine delegados multidifusión.
Los eventos son un mecanismo para publicar notificaciones o suscribirse a ellas.
Suscríbase a eventos y cancele la suscripción a estos.
Implemente eventos declarados en interfaces.
Garantice la conformidad a las directrices de .NET cuando el código publica los eventos.
Genere eventos definidos en las clases base a partir de clases derivadas.
Implemente descriptores de acceso de eventos personalizados.
Prácticas de LINQ
LINQ permite escribir código para consultar cualquier origen de datos que admita su patrón de expresión de
consultas. Estos artículos le ayudarán a comprender el patrón y a trabajar con orígenes de datos diferentes.
Consulte una colección.
Use expresiones lambda en una consulta.
Use var en expresiones de consulta.
Devuelva subconjuntos de propiedades de elementos de una consulta.
Escriba consultas con filtrado complejo.
Ordene los elementos de un origen de datos.
Ordene elementos en varias claves.
Controle el tipo de una proyección.
Cuente las repeticiones de un valor en una secuencia de origen.
Calcule valores intermedios.
Combine datos de varios orígenes.
Encuentre la diferencia de conjuntos entre dos secuencias.
Depure los resultados vacíos de una consulta.
Agregue métodos personalizados para las consultas de LINQ.
El método String.Split crea una matriz de subcadenas mediante la división de la cadena de entrada en función de
uno o varios delimitadores. A menudo, este método es la manera más fácil de separar una cadena en límites de
palabras. También sirve para dividir las cadenas en otras cadenas o caracteres específicos.
NOTE
Los ejemplos de C# de este artículo se ejecutan en el ejecutor de código en línea y área de juegos de Try.NET. Haga clic en
el botón Ejecutar para ejecutar un ejemplo en una ventana interactiva. Una vez que se ejecuta el código, puede modificar
y ejecutar el código modificado si vuelve a hacer clic en Ejecutar . El código modificado se ejecuta en la ventana interactiva
o, si se produce un error en la compilación, en la ventana interactiva se muestran todos los mensajes de error del
compilador de C#.
Este código divide una frase común en una matriz de cadenas para cada palabra.
string phrase = "The quick brown fox jumps over the lazy dog.";
string[] words = phrase.Split(' ');
Todas las instancias de un carácter separador generan un valor en la matriz devuelta. Los caracteres separadores
consecutivos generan la cadena vacía como un valor en la matriz devuelta. Puede ver cómo se crea una cadena
vacía en el ejemplo siguiente, en el que se usa el carácter de espacio como separador.
string phrase = "The quick brown fox jumps over the lazy dog.";
string[] words = phrase.Split(' ');
Este comportamiento facilita formatos como los de los archivos de valores separados por comas (CSV) que
representan datos tabulares. Las comas consecutivas representan una columna en blanco.
Puede pasar un parámetro StringSplitOptions.RemoveEmptyEntries opcional para excluir cualquier cadena vacía
en la matriz devuelta. Para un procesamiento más complicado de la colección devuelta, puede usar LINQ para
manipular la secuencia de resultados.
String.Split puede usar varios caracteres separadores. En este ejemplo se usan espacios, comas, puntos, dos
puntos y tabulaciones como caracteres de separación, que se pasan a Split en una matriz. En el bucle al final del
código se muestra cada una de las palabras de la matriz devuelta.
char[] delimiterChars = { ' ', ',', '.', ':', '\t' };
Las instancias consecutivas de cualquier separador generan la cadena vacía en la matriz de salida:
String.Split puede tomar una matriz de cadenas (secuencias de caracteres que actúan como separadores para
analizar la cadena de destino, en lugar de caracteres individuales).
Vea también
Extracción de elementos de una cadena
Guía de programación de C#
Cadenas
Expresiones regulares de .NET
Procedimiento para concatenar varias cadenas
(Guía de C#)
16/09/2021 • 4 minutes to read
Concatenación es el proceso de anexar una cadena al final de otra cadena. Las cadenas se concatenan con el
operador + . En el caso de los literales y las constantes de cadena, la concatenación se produce en tiempo de
compilación, y no en tiempo de ejecución. En cambio, para las variables de cadena, la concatenación solo se
produce en tiempo de ejecución.
NOTE
Los ejemplos de C# de este artículo se ejecutan en el ejecutor de código en línea y área de juegos de Try.NET. Haga clic en
el botón Ejecutar para ejecutar un ejemplo en una ventana interactiva. Una vez que se ejecuta el código, puede modificar
y ejecutar el código modificado si vuelve a hacer clic en Ejecutar . El código modificado se ejecuta en la ventana interactiva
o, si se produce un error en la compilación, en la ventana interactiva se muestran todos los mensajes de error del
compilador de C#.
Literales de cadena
En el ejemplo siguiente se divide un literal de cadena larga en cadenas más pequeñas para mejorar la legibilidad
en el código fuente. El código concatena las cadenas más pequeñas para crear el literal de cadena larga. Los
elementos se concatenan en una sola cadena en tiempo de compilación. No existe ningún costo de rendimiento
en tiempo de ejecución independientemente del número de cadenas implicadas.
System.Console.WriteLine(text);
Operadores + y +=
Para concatenar variables de cadena, puede usar los operadores + o += , la interpolación de cadena o los
métodos String.Format, String.Concat, String.Join o StringBuilder.Append. El operador + es sencillo de usar y
genera un código intuitivo. Aunque use varios operadores + en una instrucción, el contenido de la cadena se
copiará solo una vez. En el código siguiente se muestran ejemplos del uso de los operadores + y += para
concatenar cadenas:
string userName = "<Type your name here>";
string dateString = DateTime.Today.ToShortDateString();
Interpolación de cadenas
En algunas expresiones, es más fácil concatenar cadenas mediante la interpolación de cadena, como se muestra
en este código:
NOTE
En operaciones de concatenación de cadenas, el compilador de C# trata una cadena NULL igual que una cadena vacía.
A partir de C# 10, se puede utilizar la interpolación de cadenas para inicializar una cadena constante cuando
todas las expresiones utilizadas para los marcadores de posición son también cadenas constantes.
String.Format
Otro método para concatenar cadenas es String.Format. Este método funciona bien cuando se crea una cadena a
partir de un número reducido de cadenas de componente.
StringBuilder
En otros casos, puede combinar cadenas en un bucle, donde no sabe cuántas cadenas de origen se combinan, y
el número real de cadenas de origen puede ser elevado. La clase StringBuilder se diseñó para estos escenarios.
El código siguiente usa el método Append de la clase StringBuilder para concatenar cadenas.
Puede obtener más información sobre las razones para elegir la concatenación de cadenas o sobre la clase
StringBuilder .
String.Concat o String.Join
Otra opción para combinar cadenas a partir de una colección consiste en usar el método String.Concat. Use el
método String.Join si las cadenas de origen se deben separar con un delimitador. El código siguiente combina
una matriz de palabras usando ambos métodos:
string[] words = { "The", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog." };
LINQ y Enumerable.Aggregate
Por último, puede usar LINQ y el método Enumerable.Aggregate para combinar cadenas a partir de una
colección. Este método combina las cadenas de origen mediante una expresión lambda. La expresión lambda
realiza el trabajo de agregar cada cadena a la acumulación existente. En el ejemplo siguiente se combina una
matriz de palabras y se agrega un espacio entre cada palabra de la matriz:
string[] words = { "The", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog." };
Esta opción puede provocar más asignaciones que otros métodos para concatenar colecciones, ya que crea una
cadena intermedia para cada iteración. Si la optimización del rendimiento es fundamental, considere la clase
StringBuilder o los métodos String.Concat o String.Join para concatenar una colección, en lugar de
Enumerable.Aggregate .
Vea también
String
StringBuilder
Guía de programación de C#
Cadenas
Cómo buscar cadenas
16/09/2021 • 4 minutes to read
Puede usar dos estrategias principales para buscar texto en cadenas. Los métodos de la clase String buscan un
texto concreto. Las expresiones regulares buscan patrones en el texto.
NOTE
Los ejemplos de C# de este artículo se ejecutan en el ejecutor de código en línea y área de juegos de Try.NET. Haga clic en
el botón Ejecutar para ejecutar un ejemplo en una ventana interactiva. Una vez que se ejecuta el código, puede modificar
y ejecutar el código modificado si vuelve a hacer clic en Ejecutar . El código modificado se ejecuta en la ventana interactiva
o, si se produce un error en la compilación, en la ventana interactiva se muestran todos los mensajes de error del
compilador de C#.
El tipo string, que es un alias de la clase System.String, proporciona una serie de métodos útiles para buscar el
contenido de una cadena. Entre ellos se encuentran Contains, StartsWith, EndsWith, IndexOf y LastIndexOf. La
clase System.Text.RegularExpressions.Regex proporciona un vocabulario completo para buscar patrones en el
texto. En este artículo, aprenderá estas técnicas y a elegir el mejor método para sus necesidades.
string factMessage = "Extension methods have all the capabilities of regular static methods.";
// For user input and strings that will be displayed to the end user,
// use the StringComparison parameter on methods that have it to specify how to match strings.
bool ignoreCaseSearchResult = factMessage.StartsWith("extension",
System.StringComparison.CurrentCultureIgnoreCase);
Console.WriteLine($"Starts with \"extension\"? {ignoreCaseSearchResult} (ignoring case)");
En el ejemplo anterior, se muestra un aspecto importante del uso de estos métodos. De manera predeterminada,
las búsquedas distinguen mayúsculas de minúsculas . El valor de enumeración
StringComparison.CurrentCultureIgnoreCase se usa para especificar que se trata de una búsqueda que no
distingue mayúsculas de minúsculas.
string factMessage = "Extension methods have all the capabilities of regular static methods.";
M O DELO SIGN IF IC A DO
if (System.Text.RegularExpressions.Regex.IsMatch(s, sPattern,
System.Text.RegularExpressions.RegexOptions.IgnoreCase))
{
Console.WriteLine($" (match for '{sPattern}' found)");
}
else
{
Console.WriteLine();
}
}
TIP
Los métodos string suelen ser mejores opciones cuando se busca una cadena exacta. Las expresiones regulares son
más adecuadas cuando se busca algún patrón en una cadena de origen.
M O DELO SIGN IF IC A DO
if (System.Text.RegularExpressions.Regex.IsMatch(s, sPattern))
{
Console.WriteLine(" - valid");
}
else
{
Console.WriteLine(" - invalid");
}
}
Este patrón de búsqueda sencillo coincide con muchas cadenas válidas. Las expresiones regulares son mejores
para buscar o validar con respecto a un patrón, en lugar de una cadena de texto sencilla.
Vea también
Guía de programación de C#
Cadenas
LINQ y cadenas
System.Text.RegularExpressions.Regex
Expresiones regulares de .NET
Lenguaje de expresiones regulares: referencia rápida
Procedimientos recomendados para el uso de cadenas en .NET
Procedimiento para modificar el contenido de
cadenas en C#
16/09/2021 • 6 minutes to read
En este artículo se muestran varias técnicas para producir una string modificando una string existente. Todas
las técnicas mostradas devuelven el resultado de las modificaciones como un objeto string nuevo. Para
demostrar que las cadenas originales y modificadas son instancias distintas, los ejemplos almacenan el
resultado en una variable nueva. Al ejecutar cada ejemplo, se puede examinar tanto el objeto string original
como el objeto string nuevo y modificado.
NOTE
Los ejemplos de C# de este artículo se ejecutan en el ejecutor de código en línea y área de juegos de Try.NET. Haga clic en
el botón Ejecutar para ejecutar un ejemplo en una ventana interactiva. Una vez que se ejecuta el código, puede modificar
y ejecutar el código modificado si vuelve a hacer clic en Ejecutar . El código modificado se ejecuta en la ventana interactiva
o, si se produce un error en la compilación, en la ventana interactiva se muestran todos los mensajes de error del
compilador de C#.
En este artículo se muestran varias técnicas. Puede reemplazar el texto existente. Puede buscar patrones y
reemplazar el texto coincidente por otro texto. Puede tratar una cadena con una secuencia de caracteres.
También puede usar métodos de conveniencia para eliminar espacios en blanco. Elija la técnica con mayor
coincidencia con el escenario.
Reemplazo de texto
Con el código siguiente se crea una cadena mediante el reemplazo de texto con un sustituto.
En el código anterior se muestra esta propiedad inmutable de las cadenas. En el ejemplo anterior puede ver que
la cadena original, source , no se ha modificado. Con el método String.Replace se crea una string que contiene
las modificaciones.
Con el método Replace se pueden reemplazar cadenas o caracteres únicos. En ambos casos, se reemplazan
todas las instancias del texto buscado. En el siguiente ejemplo se reemplazan todos los caracteres " " por "_":
La cadena de origen se mantiene y se devuelve una cadena nueva con los reemplazos.
Recorte de espacios en blanco
Puede usar los métodos String.Trim, String.TrimStart, y String.TrimEnd para quitar los espacios en blanco al inicio
y al final. En el código siguiente se muestra un ejemplo de cada caso. La cadena de origen no cambia; con estos
métodos se devuelve una cadena nueva con el contenido modificado.
Eliminación de texto
Puede quitar texto de una cadena con el método String.Remove. Este método quita un número de caracteres que
comienzan con un índice específico. En el siguiente ejemplo se muestra cómo usar String.IndexOf seguido por
Remove para quitar texto de una cadena:
Con el método StringBuilder.ToString se devuelve una cadena inmutable con el contenido del objeto
StringBuilder.
string phrase = "The quick brown fox jumps over the fence";
Console.WriteLine(phrase);
// constructing a string from a char array, prefix it with some additional characters
char[] chars = { 'a', 'b', 'c', 'd', '\0' };
int length = chars.Length + 2;
string result = string.Create(length, chars, (Span<char> strContent, char[] charArray) =>
{
strContent[0] = '0';
strContent[1] = '1';
for (int i = 0; i < charArray.Length; i++)
{
strContent[i + 2] = charArray[i];
}
});
Console.WriteLine(result);
Puede modificar una cadena en un bloque fijo con código no seguro, pero es totalmente desaconsejable
modificar el contenido de la cadena una vez que se ha creado. Si lo hace, puede haber problemas imprevisibles.
Por ejemplo, si alguien se conecta a una cadena que tiene el mismo contenido que la suya, esa persona obtendrá
la copia de usted y no esperará que usted modifique la cadena.
Vea también
Expresiones regulares de .NET
Lenguaje de expresiones regulares: referencia rápida
Cómo comparar cadenas en C#
16/09/2021 • 12 minutes to read
Se comparan cadenas para responder a una de estas dos preguntas: "¿Son iguales estas dos cadenas?" o "¿En
qué orden deben colocarse estas cadenas al ordenarlas?"
Esas dos preguntas se complican por factores que influyen en las comparaciones de cadenas:
Puede elegir una comparación ordinal o lingüística.
Puede elegir si distingue entre mayúsculas y minúsculas.
Puede elegir comparaciones específicas de referencia cultural.
Las comparaciones lingüísticas dependen de la plataforma y la referencia cultural.
NOTE
Los ejemplos de C# de este artículo se ejecutan en el ejecutor de código en línea y área de juegos de Try.NET. Haga clic en
el botón Ejecutar para ejecutar un ejemplo en una ventana interactiva. Una vez que se ejecuta el código, puede modificar
y ejecutar el código modificado si vuelve a hacer clic en Ejecutar . El código modificado se ejecuta en la ventana interactiva
o, si se produce un error en la compilación, en la ventana interactiva se muestran todos los mensajes de error del
compilador de C#.
Cuando se comparan cadenas, se define un orden entre ellas. Las comparaciones se usan para ordenar una
secuencia de cadenas. Una vez que la secuencia está en un orden conocido, es más fácil hacer búsquedas, tanto
para el software como para las personas. Otras comparaciones pueden comprobar si las cadenas son iguales.
Estas comprobaciones de similitud son parecidas a la igualdad, pero pueden omitirse algunas diferencias, como
las diferencias entre mayúsculas y minúsculas.
Console.WriteLine($"Using == says that <{root}> and <{root2}> are {(root == root2 ? "equal" : "not
equal")}");
Console.WriteLine($"Ordinal ignore case: <{root}> and <{root2}> are {(result ? "equal." : "not equal.")}");
Console.WriteLine($"Ordinal static ignore case: <{root}> and <{root2}> are {(areEqual ? "equal." : "not
equal.")}");
if (comparison < 0)
Console.WriteLine($"<{root}> is less than <{root2}>");
else if (comparison > 0)
Console.WriteLine($"<{root}> is greater than <{root2}>");
else
Console.WriteLine($"<{root}> and <{root2}> are equivalent in order");
Al realizar una comparación ordinal que distingue mayúsculas de minúsculas, estos métodos usarán las
convenciones de mayúsculas y minúsculas de la referencia cultural invariable.
Comparaciones lingüísticas
También se pueden ordenar cadenas mediante reglas lingüísticas para la referencia cultural actual. Esto se
conoce a veces como "criterio de ordenación por palabras". Cuando se realiza una comparación lingüística,
algunos caracteres Unicode no alfanuméricos pueden tener asignados pesos especiales. Por ejemplo, el guion
("-") podría tener asignado un peso pequeño, por lo que las cadenas "coop" y "co-op" aparecerían una junto a la
otra en una ordenación. Además, algunos caracteres Unicode pueden ser equivalentes a una secuencia de
instancias de Char. En este ejemplo se usa una frase en alemán que significa "Bailan en la calle", en alemán, con
"ss" (U+0073 U+0073) en una cadena y "ß" (U+00DF) en otra. Lingüísticamente (en Windows), "ss" es igual que
el carácter "ß" en alemán en las referencias culturales "en-US" y "de-DE".
string first = "Sie tanzen auf der Straße.";
string second = "Sie tanzen auf der Strasse.";
showComparison(word, words);
showComparison(word, other);
showComparison(words, other);
void showComparison(string one, string two)
{
int compareLinguistic = String.Compare(one, two, StringComparison.InvariantCulture);
int compareOrdinal = String.Compare(one, two, StringComparison.Ordinal);
if (compareLinguistic < 0)
Console.WriteLine($"<{one}> is less than <{two}> using invariant culture");
else if (compareLinguistic > 0)
Console.WriteLine($"<{one}> is greater than <{two}> using invariant culture");
else
Console.WriteLine($"<{one}> and <{two}> are equivalent in order using invariant culture");
if (compareOrdinal < 0)
Console.WriteLine($"<{one}> is less than <{two}> using ordinal comparison");
else if (compareOrdinal > 0)
Console.WriteLine($"<{one}> is greater than <{two}> using ordinal comparison");
else
Console.WriteLine($"<{one}> and <{two}> are equivalent in order using ordinal comparison");
}
Este ejemplo demuestra la naturaleza dependiente del sistema operativo de las comparaciones lingüísticas. El
host de la ventana interactiva es un host Linux. Las comparaciones lingüísticas y ordinales producen el mismo
resultado. Si se ejecuta este mismo ejemplo en un host de Windows, verá este resultado:
En Windows, el criterio de ordenación de "cop", "coop" y "co-op" cambia al cambiar de una comparación
lingüística a una comparación ordinal. Las dos frases en alemán también se comparan de manera diferente
mediante tipos de comparación diferentes.
Las comparaciones dependientes de la referencia cultural se usan normalmente para comparar y ordenar
cadenas escritas por usuarios con otras cadenas escritas por usuarios. Los caracteres y las convenciones de
ordenación de estas cadenas pueden variar según la configuración regional del equipo del usuario. Incluso las
cadenas que contienen caracteres idénticos podrían ordenarse de forma diferente en función de la referencia
cultural del subproceso actual. Además, pruebe este código de ejemplo localmente en una máquina con
Windows y obtendrá estos resultados:
Las comparaciones lingüísticas dependen de la referencia cultural actual y del sistema operativo. Téngalo en
cuenta cuando trabaje con comparaciones de cadenas.
Ordenación lingüística y búsqueda de cadenas en matrices
En estos ejemplos se muestra cómo ordenar y buscar cadenas en una matriz mediante una comparación
lingüística que depende de la referencia cultural actual. Use los métodos Array estáticos que toman un
parámetro System.StringComparer.
En este ejemplo se muestra cómo ordenar una matriz de cadenas con la referencia cultural actual:
Console.WriteLine("Non-sorted order:");
foreach (string s in lines)
{
Console.WriteLine($" {s}");
}
Console.WriteLine("\n\rSorted order:");
Una vez que se ordena la matriz, puede buscar entradas mediante una búsqueda binaria. Una búsqueda binaria
empieza en medio de la colección para determinar qué mitad de la colección debe contener la cadena buscada.
Cada comparación posterior divide la parte restante de la colección por la mitad. La matriz se ordena con el
elemento StringComparer.CurrentCulture. La función local ShowWhere muestra información sobre dónde se
encuentra la cadena. Si no se encuentra la cadena, el valor devuelto indica dónde estaría si se encontrara.
string[] lines = new string[]
{
@"c:\public\textfile.txt",
@"c:\public\textFile.TXT",
@"c:\public\Text.txt",
@"c:\public\testfile2.txt"
};
Array.Sort(lines, StringComparer.CurrentCulture);
if (index == 0)
Console.Write("beginning of sequence and ");
else
Console.Write($"{array[index - 1]} and ");
if (index == array.Length)
Console.WriteLine("end of sequence.");
else
Console.WriteLine($"{array[index]}.");
}
else
{
Console.WriteLine($"Found at index {index}.");
}
}
Console.WriteLine("Non-sorted order:");
foreach (string s in lines)
{
Console.WriteLine($" {s}");
}
Console.WriteLine("\n\rSorted order:");
Una vez realizada la ordenación, se pueden hacer búsquedas en la lista de cadenas mediante una búsqueda
binaria. En este ejemplo se muestra cómo buscar la lista ordenada usando la misma función de comparación. La
función local ShowWhere muestra dónde está o debería estar el texto buscado:
List<string> lines = new List<string>
{
@"c:\public\textfile.txt",
@"c:\public\textFile.TXT",
@"c:\public\Text.txt",
@"c:\public\testfile2.txt"
};
lines.Sort((left, right) => left.CompareTo(right));
if (index == 0)
Console.Write("beginning of sequence and ");
else
Console.Write($"{collection[index - 1]} and ");
if (index == collection.Count)
Console.WriteLine("end of sequence.");
else
Console.WriteLine($"{collection[index]}.");
}
else
{
Console.WriteLine($"Found at index {index}.");
}
}
Asegúrese siempre de usar el mismo tipo de comparación para la ordenación y la búsqueda. Si se usan distintos
tipos de comparación para la ordenación y la búsqueda se producen resultados inesperados.
Las clases de colección como System.Collections.Hashtable,
System.Collections.Generic.Dictionary<TKey,TValue> y System.Collections.Generic.List<T> tienen constructores
que toman un parámetro System.StringComparer cuando el tipo de los elementos o claves es string . En
general, debe usar estos constructores siempre que sea posible y especificar StringComparer.Ordinal u
StringComparer.OrdinalIgnoreCase.
if (String.ReferenceEquals(a, b))
Console.WriteLine("a and b are interned.");
else
Console.WriteLine("a and b are not interned.");
string c = String.Copy(a);
if (String.ReferenceEquals(a, c))
Console.WriteLine("a and c are interned.");
else
Console.WriteLine("a and c are not interned.");
NOTE
Cuando se prueba la igualdad de cadenas, debe usar los métodos que especifican explícitamente el tipo de comparación
que va a realizar. El código se vuelve mucho más legible y fácil de mantener. Use sobrecargas de los métodos de las clases
System.String y System.Array que toman un parámetro de enumeración StringComparison. Especifique qué tipo de
comparación se va a realizar. Evite usar los operadores == y != cuando pruebe la igualdad. Los métodos de instancia
String.CompareTo siempre realizan una comparación ordinal con distinción entre mayúsculas y minúsculas. Son adecuados
principalmente para ordenar alfabéticamente las cadenas.
Puede internalizar una cadena o recuperar una referencia a una cadena internalizada existente llamando al
método String.Intern. Para determinar si una cadena se aplica el método Intern, llame al método
String.IsInterned.
Vea también
System.Globalization.CultureInfo
System.StringComparer
Cadenas
Comparación de cadenas
Globalizar y localizar aplicaciones
Procedimiento para detectar excepciones no
compatibles con CLS
16/09/2021 • 2 minutes to read
Algunos lenguajes. NET, incluido C++/CLI, permiten que los objetos inicien excepciones que no se derivan de
Exception. Dichas excepciones se denominan excepciones de no compatibilidad con CLS o no excepciones. En C#
no se pueden producir excepciones de no compatibilidad con CLS, pero se pueden detectar de dos formas:
Dentro de un bloque catch (RuntimeWrappedException e) .
De forma predeterminada, un ensamblado de Visual C# detecta las excepciones de no compatibilidad con
CLS como excepciones ajustadas. Use este método si necesita tener acceso a la excepción original, a la
que se puede tener acceso a través de la propiedad RuntimeWrappedException.WrappedException. El
procedimiento mostrado más adelante en este tema explica cómo detectar las excepciones de esta
manera.
Dentro de un bloque catch general (un bloque catch sin un tipo de excepción especificado) que se coloca
detrás de todos los demás bloques catch .
Use este método cuando quiera realizar alguna acción (como escribir en un archivo de registro) en
respuesta a las excepciones de no compatibilidad con CLS y no necesita tener acceso a la información de
excepción. De forma predeterminada, el Common Language Runtime ajusta todas las excepciones. Para
deshabilitar este comportamiento, agregue este atributo de nivel de ensamblado en el código,
normalmente en el archivo AssemblyInfo.cs:
[assembly: RuntimeCompatibilityAttribute(WrapNonExceptionThrows = false)] .
Ejemplo
En el ejemplo siguiente se muestra cómo detectar una excepción de no compatibilidad con CLS iniciada desde
una biblioteca de clases escrita en C++/CLI. Tenga en cuenta que en este ejemplo, el código de cliente de C#
conoce por adelantado que el tipo de excepción que se inicia es System.String. Puede convertir la propiedad
RuntimeWrappedException.WrappedException de vuelta a su tipo original siempre que el tipo sea accesible
desde su código.
// Class library written in C++/CLI.
var myClass = new ThrowNonCLS.Class1();
try
{
// throws gcnew System::String(
// "I do not derive from System.Exception!");
myClass.TestThrow();
}
catch (RuntimeWrappedException e)
{
String s = e.WrappedException as String;
if (s != null)
{
Console.WriteLine(s);
}
}
Consulte también
RuntimeWrappedException
Excepciones y control de excepciones
SDK de .NET Compiler Platform
16/09/2021 • 7 minutes to read
Los compiladores crean un modelo detallado de código de aplicación a medida que validan la sintaxis y
semántica de ese código. Utilizan este modelo para generar la salida ejecutable desde el código fuente. El SDK
de .NET Compiler Platform proporciona acceso a este modelo. Cada vez confiamos más en las características del
entorno de desarrollo integrado (IDE), como IntelliSense, la refactorización, el cambio de nombre inteligente,
"Buscar todas las referencias" e "Ir a definición" para aumentar la productividad. Nos basamos en herramientas
de análisis de código para mejorar la calidad de nuestro código y en generadores de código para ayudar en la
construcción de la aplicación. A medida que la inteligencia de estas herramientas aumenta, cada vez necesitan
más acceso del modelo que solo los compiladores crean cuando procesan el código de la aplicación. Es la
misión principal de las API de Roslyn: abrir las cajas negras y permitir que herramientas y usuarios finales
compartan la gran cantidad de información que los compiladores tienen sobre el código. En lugar de actuar
como traductores opacos de código fuente de entrada en código objeto de salida, con Roslyn, los compiladores
se convierten en plataformas: API que puede usar para tareas relacionadas con el código en las herramientas y
aplicaciones.
TIP
Antes de compilar su propio analizador, consulte los integrados. Para obtener más información, vea Reglas de estilo del
código.
Los compiladores procesan el código que se escribe conforme a reglas estructuradas que a menudo se
diferencian de la forma en que los seres humanos lo leen y lo entienden. Una comprensión básica del modelo
usado por los compiladores resulta fundamental para entender las API que se usan al compilar herramientas
basadas en Roslyn.
Cada fase de esta canalización es un componente independiente. En primer lugar, la fase de análisis acorta y
analiza el texto de origen en la sintaxis que sigue la gramática del lenguaje. En segundo lugar, la fase de
declaración analiza los metadatos importados y de origen para formar símbolos con nombre. Luego, la fase de
enlace combina los identificadores del código con los símbolos. Por último, en la fase de emisión se emite un
ensamblado con toda la información generada por el compilador.
En correspondencia a cada una de esas fases, el SDK de .NET Compiler Platform expone un modelo de objetos
que permite el acceso a la información en esa fase. La fase de análisis expone un árbol de sintaxis, la fase de
declaración expone una tabla de símbolos jerárquica, la fase de enlace expone el resultado del análisis
semántico del compilador y la fase de emisión es una API que genera códigos de bytes de IL.
Cada compilador combina estos componentes como un único todo.
Estas API son las mismas que usa Visual Studio. Por ejemplo, las características de formato y esquema del
código usan los árboles de sintaxis, las características de navegación y el Examinador de objetos usan la tabla
de símbolos, las refactorizaciones e Ir a definición usan el modelo semántico, y Editar y continuar usa todos
ellos, incluida la API de emisión.
Capas de API
El SDK del compilador de .NET consta de varias capas de API: de compilador, de diagnóstico, de scripting y de
área de trabajo.
API de compilador
La capa de compilador contiene los modelos de objetos que corresponden con la información expuesta en cada
fase de la canalización de compilador, sintáctica y semántica. La capa de compilador también contiene una
instantánea inmutable de una sola invocación de un compilador, incluidas las referencias de ensamblado, las
opciones del compilador y los archivos de código fuente. Hay dos API distintas que representan el lenguaje C# y
el lenguaje Visual Basic. Las dos API son similares en forma, aunque están adaptadas para lograr una alta
fidelidad con cada lenguaje. Esta capa no tiene dependencias en componentes de Visual Studio.
API de diagnóstico
Como parte de su análisis, el compilador puede generar un conjunto de diagnósticos que abarcan desde errores
de sintaxis, semántica y asignación definitiva hasta distintas advertencias y diagnósticos informativos. La capa
de la API de compilador expone diagnósticos a través de una API extensible que permite incluir analizadores
definidos por el usuario en el proceso de compilación. Permite generar diagnósticos definidos por el usuario,
como los de herramientas como StyleCop, junto con diagnósticos definidos por el compilador. La generación de
diagnósticos de este modo tiene la ventaja de integrarse de forma natural con herramientas como MSBuild y
Visual Studio, que dependen de diagnósticos de experiencias como detener una compilación según una directiva
y mostrar subrayados ondulados activos en el editor y sugerir correcciones de código.
API de scripting
Las API de hospedaje y scripting forman parte de la capa de compilador. Puede usarlas para ejecutar fragmentos
de código y acumular un contexto de ejecución en tiempo de ejecución. El REPL (bucle de lectura, evaluación e
impresión) interactivo de C# usa estas API. El REPL permite usar C# como lenguaje de scripting, al ejecutar el
código de forma interactiva mientras se escribe.
API de áreas de trabajo
La capa de áreas de trabajo contiene la API de área de trabajo, que es el punto de partida para realizar análisis
de código y refactorización de soluciones completas. Ayuda a organizar toda la información sobre los proyectos
de una solución en un modelo de objetos único, lo que ofrece acceso directo a los modelos de objetos de capa
de compilador sin necesidad de analizar archivos, configurar opciones ni administrar dependencias entre
proyectos.
Además, la capa de áreas de trabajo expone un conjunto de API que se usa al implementar las herramientas de
análisis de código y refactorización que funcionan en un entorno de host como el IDE de Visual Studio. Los
ejemplos incluyen las API Buscar todas las referencias, Formato y Generación de código.
Esta capa no tiene dependencias en componentes de Visual Studio.
Trabajar con sintaxis
16/09/2021 • 8 minutes to read
El árbol de sintaxis es una estructura de datos fundamental e inmutable expuesta por las API del compilador.
Estos árboles representan la estructura léxica y sintáctica del código fuente. Tienen dos importantes finalidades:
Permitir herramientas, como un IDE, complementos, herramientas de análisis de código y refactorizaciones, y
ver y procesar la estructura sintáctica del código fuente del proyecto de un usuario.
Habilitar herramientas, como refactorizaciones y un IDE, para crear, modificar y reorganizar el código fuente
de forma natural sin tener que usar ediciones de texto directas. Al crear y manipular árboles, las
herramientas pueden crear y reorganizar fácilmente el código fuente.
Árboles de sintaxis
Los árboles de sintaxis son la estructura principal usada para la compilación, el análisis de código, los enlaces, la
refactorización, las características de IDE y la generación de código. Ninguna parte del código fuente se entiende
sin que primero se haya identificado y clasificado en alguno de los elementos de lenguaje estructural conocidos.
Los árboles de sintaxis tienen tres atributos clave:
Contienen toda la información de origen con fidelidad completa. Esto significa que el árbol de sintaxis
contiene cada fragmento de información del texto de origen, cada construcción gramatical, cada token léxico
y todo lo demás, incluidos los espacios en blanco, los comentarios y las directivas de preprocesador. Por
ejemplo, cada literal mencionado en el origen se representa exactamente como se ha escrito. Los árboles de
sintaxis también capturan los errores del código fuente cuando el programa está incompleto o tiene un
formato incorrecto mediante la representación de los tokens omitidos o que faltan.
Pueden generar el texto exacto mediante el que se analizaron. Es posible obtener la representación de texto
del subárbol cuya raíz está en ese nodo desde cualquier nodo de sintaxis. Esta posibilidad significa que los
árboles de sintaxis se pueden usar como una manera de crear y editar texto de origen. Al crear un árbol, de
manera implícita, ha creado el texto equivalente, mientras que, al crear uno a partir de los cambios en uno
existente, ha editado realmente el texto.
Son inmutables y seguros para subprocesos. Una vez obtenido un árbol, es una instantánea del estado actual
del código y nunca cambia. Esto permite que varios usuarios interactúen con el mismo árbol de sintaxis a la
vez en distintos subprocesos sin que se produzca ningún bloqueo ni duplicación. Dado que los árboles son
inmutables y no permiten ninguna modificación directa, los métodos de fábrica ayudan a crear y modificar
los árboles de sintaxis mediante la creación de instantáneas adicionales del árbol. Los árboles son eficaces en
su forma de volver a usar nodos subyacentes, así que es posible volver a crear una nueva versión
rápidamente y con poca memoria adicional.
Un árbol de sintaxis es literalmente una estructura de datos de árbol donde los elementos estructurales no
terminales son primarios con respecto a otros elementos. Cada árbol de sintaxis se compone de nodos, tokens y
curiosidades.
Nodos de sintaxis
Los nodos de sintaxis son uno de los elementos principales de los árboles de sintaxis. Estos nodos representan
construcciones sintácticas como declaraciones, instrucciones, cláusulas y expresiones. Cada categoría de nodos
de sintaxis se representa mediante una clase independiente derivada de Microsoft.CodeAnalysis.SyntaxNode. El
conjunto de clases de nodos no es extensible.
Todos los nodos de sintaxis son nodos no terminales del árbol de sintaxis, lo que significa que siempre tienen
otros nodos y tokens como elementos secundarios. Como elemento secundario de otro nodo, cada nodo tiene
un nodo principal al que se puede acceder mediante la propiedad SyntaxNode.Parent. Dado que los nodos y los
árboles son inmutables, el elemento principal de un nodo nunca cambia. La raíz del árbol tiene un elemento
principal nulo.
Cada nodo tiene un método SyntaxNode.ChildNodes() que devuelve una lista de nodos secundarios en orden
secuencial según su posición en el texto de origen. Esta lista no contiene tokens. Cada nodo también tiene
métodos para examinar descendientes, como DescendantNodes, DescendantTokens o DescendantTrivia, que
representan una lista de todos los nodos, tokens o curiosidades que existen en el subárbol cuya raíz está en ese
nodo.
Además, cada subclase de nodos de sintaxis expone los mismos elementos secundarios mediante propiedades
fuertemente tipadas. Por ejemplo, una clase de nodos BinaryExpressionSyntax tiene tres propiedades
adicionales específicas de los operadores binarios: Left, OperatorToken y Right. El tipo de Left y Right es
ExpressionSyntax, y el tipo de OperatorToken es SyntaxToken.
Algunos nodos de sintaxis tienen elementos secundarios opcionales. Por ejemplo, IfStatementSyntax tiene un
elemento opcional ElseClauseSyntax. Si el elemento secundario no está presente, la propiedad devuelve null.
Tokens de sintaxis
Los tokens de sintaxis son los terminales de la gramática del lenguaje y representan los fragmentos sintácticos
más pequeños del código. Nunca son elementos principales de otros nodos o tokens. Los tokens de sintaxis
constan de palabras clave, identificadores, literales y signos de puntuación.
Por eficacia, el tipo SyntaxToken es un tipo de valor CLR. Por tanto, a diferencia de los nodos de sintaxis, solo hay
una estructura para todos los tipos de tokens con una mezcla de propiedades que tienen significado según el
tipo de token que se va a representar.
Por ejemplo, un token de literal entero representa un valor numérico. Además del texto de origen sin formato
que abarca el token, el token de literal tiene una propiedad Value que indica el valor entero descodificado exacto.
El tipo de esta propiedad se considera Object, ya que puede ser alguno de los tipos primitivos.
La propiedad ValueText transmite la misma información que la propiedad Value; pero el tipo de esta propiedad
siempre es String. Un identificador en el texto de origen C# puede incluir caracteres de escape Unicode, aunque
la sintaxis de la propia secuencia de escape no se considera parte del nombre del identificador. Por tanto,
aunque el texto sin formato que abarca el token incluye la secuencia de escape, la propiedad ValueText no. En su
lugar, incluye los caracteres Unicode que identifica el escape. Por ejemplo, si el texto de origen contiene un
identificador escrito como \u03C0 , la propiedad ValueText de este token devuelve π .
Curiosidades de sintaxis
Las curiosidades de sintaxis representan las partes del texto de origen que no son realmente significativas para
la correcta comprensión del código, como los espacios en blanco, los comentarios y las directivas de
preprocesador. Al igual que los tokens de sintaxis, las curiosidades son tipos de valor. Se usa el tipo único
Microsoft.CodeAnalysis.SyntaxTrivia para describir todos los tipos de curiosidades.
Dado que las curiosidades no forman parte de la sintaxis normal del lenguaje y pueden aparecer en cualquier
lugar entre dos tokens, no se incluyen en el árbol de sintaxis como elemento secundario de un nodo. Pero dado
que son importantes a la hora de implementar una característica como la refactorización y de mantener la plena
fidelidad con el texto de origen, existen como parte del árbol de sintaxis.
Puede acceder a las curiosidades si inspecciona las colecciones SyntaxToken.LeadingTrivia o
SyntaxToken.TrailingTrivia de un token. Cuando se analiza el texto de origen, las secuencias de curiosidades se
asocian a los tokens. En general, un token es propietario de cualquier curiosidad que le preceda en la misma
línea hasta el siguiente token. Cualquier curiosidad situada después de esa línea se asocia al token siguiente. El
primer token del archivo de origen obtiene todas las curiosidades iniciales, mientras que la última secuencia de
curiosidades del archivo se agrega al último token del archivo, que, de lo contrario, tiene ancho de cero.
A diferencia de los nodos y los tokens de sintaxis, las curiosidades de sintaxis no tienen elementos principales.
Pero, dado que forman parte del árbol y cada una está asociada a un token único, se puede acceder al token con
el que está asociada mediante la propiedad SyntaxTrivia.Token.
Intervalos
Cada nodo, token o curiosidad conoce su posición dentro del texto de origen y el número de caracteres del que
se compone. Una posición de texto se representa como un entero de 32 bits, que es un índice char de base
cero. Un objeto TextSpan es la posición inicial y un recuento de caracteres, ambos representados como enteros.
Si TextSpan tiene una longitud cero, hace referencia a una ubicación entre dos caracteres.
Cada nodo tiene dos propiedades TextSpan: Span y FullSpan.
La propiedad Span es el intervalo de texto desde el principio del primer token del subárbol del nodo al final del
último token. Este intervalo no incluye ninguna curiosidad inicial ni final.
La propiedad FullSpan es el intervalo de texto que incluye el intervalo normal del nodo, así como el intervalo de
cualquier curiosidad inicial o final.
Por ejemplo:
if (x > 3)
{
|| // this is bad
|throw new Exception("Not right.");| // better exception?||
}
El nodo de la instrucción dentro del bloque tiene un intervalo indicado por las plecas (|). Incluye los caracteres
throw new Exception("Not right."); . Las plecas dobles (||) indican el intervalo completo. Incluye los mismos
caracteres que el intervalo y los caracteres asociados a las curiosidades inicial y final.
Tipos
Cada nodo, token o curiosidad tiene una propiedad SyntaxNode.RawKind, de tipo System.Int32, que identifica el
elemento de sintaxis exacto representado. Este valor se puede convertir en una enumeración específica del
lenguaje. Cada lenguaje, C# o Visual Basic, tiene una sola enumeración SyntaxKind
(Microsoft.CodeAnalysis.CSharp.SyntaxKind y Microsoft.CodeAnalysis.VisualBasic.SyntaxKind, respectivamente)
que enumera todos los posibles nodos, tokens y curiosidades de la gramática. Dicha conversión se puede
realizar automáticamente. Para ello, es necesario acceder a los métodos de extensión CSharpExtensions.Kind o
VisualBasicExtensions.Kind.
La propiedad RawKind permite anular fácilmente la ambigüedad de los tipos de nodos de sintaxis que
comparten la misma clase de nodos. En el caso de los tokens y las curiosidades, esta propiedad es la única
manera de distinguir un tipo de elemento de otro.
Por ejemplo, una sola clase BinaryExpressionSyntax tiene Left, OperatorToken y Right como elementos
secundarios. La propiedad Kind distingue si es un tipo AddExpression, SubtractExpression o MultiplyExpression
de nodo de sintaxis.
TIP
Se recomienda comprobar los tipos con los métodos de extensión IsKind (para C#) o IsKind (para VB).
Errores
Aunque el texto de origen contenga errores de sintaxis, se expone un árbol de sintaxis completo con recorrido
de ida y vuelta al origen. Si el analizador detecta código que no se ajusta a la sintaxis definida del lenguaje, usa
una de estas dos técnicas para crear un árbol de sintaxis.
Si el analizador espera un determinado tipo de token, pero no lo encuentra, puede insertar un token que
falta en el árbol de sintaxis en la ubicación esperada del token. Un token que falta representa el token real
esperado, aunque tiene un intervalo vacío y su propiedad SyntaxNode.IsMissing devuelve true .
El analizador puede omitir tokens hasta encontrar uno en el que pueda seguir analizando. En este caso,
los tokens omitidos se adjuntan como un nodo de curiosidades con el tipo SkippedTokensTrivia.
Trabajar con semántica
16/09/2021 • 3 minutes to read
Los árboles de sintaxis representan la estructura léxica y sintáctica del código fuente. Aunque esta información
por sí misma basta para describir todas las declaraciones y la lógica del origen, no es suficiente para identificar
aquello a lo que se hace referencia. Un nombre puede representar:
un tipo
un campo
un método
una variable local
Aunque cada uno de ellos es exclusivamente diferente, determinar a cuál hace referencia realmente un
identificador suele exigir una comprensión profunda de las reglas del lenguaje.
Hay elementos de programa representados en el código fuente y, además, los programas pueden hacer
referencia a bibliotecas compiladas anteriormente, empaquetadas en archivos de ensamblado. Aunque no hay
ningún código fuente y, por tanto, ningún nodo ni árbol de sintaxis, disponible para los ensamblados, los
programas aún pueden hacer referencia a los elementos incluidos en ellos.
Para esas tareas necesita el modelo semántico .
Además de un modelo sintáctico del código fuente, un modelo semántico encapsula las reglas del lenguaje, lo
que le ofrece una manera sencilla de combinar correctamente los identificadores con el elemento de programa
correcto al que se hace referencia.
Compilación
Una compilación es una representación de todo lo necesario para compilar un programa de C# o Visual Basic, lo
que incluye todas las referencias de ensamblado, las opciones de compilador y los archivos de origen.
Dado que toda esta información está en un solo lugar, los elementos incluidos en el código fuente pueden
describirse con más detalle. La compilación representa cada tipo declarado, miembro o variable como un
símbolo. La compilación contiene una serie de métodos que ayudan a encontrar y relacionar los símbolos que
se han declarado en el código fuente o importado como metadatos desde un ensamblado.
Al igual que los árboles de sintaxis, las compilaciones son inmutables. Después de crear una compilación, ni el
usuario ni nadie con quien la comparta puede modificarla. Pero puede crear una nueva compilación a partir de
una existente, al especificar un cambio a medida que lo realiza. Por ejemplo, podría crear una compilación igual
en todos los sentidos a una compilación existente, salvo que podría incluir un archivo de origen adicional o una
referencia de ensamblado.
Símbolos
Un símbolo representa un elemento diferenciado declarado por el código fuente o importado desde un
ensamblado como metadatos. Cada espacio de nombres, tipo, método, propiedad, campo, evento, parámetro o
variable local se representa mediante un símbolo.
Una serie de métodos y propiedades del tipo Compilation ayudan a encontrar símbolos. Por ejemplo, puede
buscar el símbolo de un tipo declarado por su nombre de metadatos común. También puede acceder a la tabla
de símbolos completa como un árbol de símbolos enraizado por el espacio de nombres global.
Los símbolos también contienen información adicional que el compilador determina desde el origen o los
metadatos, como otros símbolos referenciados. Cada tipo de símbolo se representa mediante una interfaz
independiente derivada de ISymbol, cada una con sus propios métodos y propiedades que detallan la
información recopilada por el compilador. Muchas de estas propiedades hacen referencia directamente a otros
símbolos. Por ejemplo, la propiedad IMethodSymbol.ReturnType indica el símbolo de tipo real que devuelve el
método.
Los símbolos presentan una representación común de espacios de nombres, tipos y miembros, entre el código
fuente y los metadatos. Por ejemplo, un método que se ha declarado en el código fuente y un método que se ha
importado desde los metadatos se representan mediante un elemento IMethodSymbol con las mismas
propiedades.
Los símbolos son similares en concepto al sistema de tipos de CLR representado por la API System.Reflection,
aunque son mejores en el aspecto de que modelan algo más que tipos. Los espacios de nombres, las variables
locales y las etiquetas son todos símbolos. Además, los símbolos son una representación de conceptos del
lenguaje, no de conceptos de CLR. Hay mucha superposición, pero también muchas distinciones significativas.
Por ejemplo, un método Iterator de C# o Visual Basic es un único símbolo. Pero si el método Iterator se traduce a
metadatos de CLR, es un tipo y varios métodos.
Modelo semántico
Un modelo semántico representa toda la información semántica de un solo archivo de origen. Puede usarlo
para descubrir lo siguiente:
Los símbolos a los que se hace referencia en una ubicación concreta del origen.
El tipo resultante de cualquier expresión.
Todos los diagnósticos, que son errores y advertencias.
Cómo fluyen las variables hacia y desde las regiones del origen.
Las respuestas a preguntas más especulativas.
Trabajar con un área de trabajo
16/09/2021 • 3 minutes to read
La capa Áreas de trabajo es el punto de partida para realizar análisis de código y refactorización de soluciones
completas. En esta capa, la API de área de trabajo ayuda a organizar toda la información sobre los proyectos de
una solución en un modelo de objetos único, lo que ofrece acceso directo a modelos de objetos de capa de
compilador como texto de origen, árboles de sintaxis, modelos semánticos y compilaciones sin necesidad de
analizar archivos, configurar opciones ni administrar dependencias entre proyectos.
Los entornos de host, como un IDE, proporcionan un área de trabajo que corresponde a la solución abierta.
También es posible usar este modelo fuera de un IDE con solo cargar un archivo de solución.
Área de trabajo
Un área de trabajo es una representación activa de la solución como una colección de proyectos, cada uno con
una colección de documentos. Normalmente, un área de trabajo está asociada a un entorno de host en continuo
cambio a medida que el usuario escribe o manipula las propiedades.
Workspace proporciona acceso al modelo actual de la solución. Cuando se produce un cambio en el entorno de
host, el área de trabajo desencadena los eventos correspondientes y la propiedad Workspace.CurrentSolution se
actualiza. Por ejemplo, cuando el usuario escribe en un editor de texto correspondiente a uno de los documentos
de origen, el área de trabajo usa un evento para indicar que ha cambiado el modelo general de la solución y qué
documento se ha modificado. Luego puede reaccionar a esos cambios mediante el análisis de la corrección del
nuevo modelo, resaltando las áreas de importancia o realizando una sugerencia para un cambio de código.
También puede crear áreas de trabajo independientes desconectadas del entorno de host o que se usen en una
aplicación sin entorno de host.
En este artículo se ofrece información general de la herramienta Visualizador de sintaxis que se incluye como
parte del SDK de .NET Compiler Platform ("Roslyn"). El Visualizador de sintaxis es una ventana de herramientas
con la que puede inspeccionar y explorar árboles de sintaxis. Es una herramienta esencial para comprender los
modelos de código que quiere analizar. También es útil para la depuración al desarrollar sus propias aplicaciones
con el SDK de .NET Compiler Platform (“Roslyn”). Abra esta herramienta cuando vaya a crear sus primeros
analizadores. Con el visualizador comprenderá los modelos usados por las API. También puede usar
herramientas como SharpLab o LINQPad para inspeccionar el código y comprender los árboles de sintaxis.
Visualizador de sintaxis
Syntax Visualizer permite inspeccionar el árbol de sintaxis del archivo de código de C# o Visual Basic en la
ventana del editor activo actual en el IDE de Visual Studio. El visualizador se puede iniciar haciendo clic en Vista
> Other Windows (Otras ventanas) > Syntax Visualizer (Visualizador de sintaxis) . También puede
usar la barra de herramientas Inicio rápido en la esquina superior derecha. Escriba "syntax" y se mostrará el
comando para abrir el Visualizador de sintaxis .
Este comando abre el Visualizador de sintaxis como una ventana de herramientas flotante. Si no tiene abierta
ninguna ventana de editor de código, la presentación está en blanco, tal como se muestra en esta imagen.
Acople esta ventana de herramienta en una ubicación cómoda dentro de Visual Studio, como por ejemplo, en el
lado izquierdo. El visualizador muestra información sobre el archivo de código actual.
Cree un nuevo proyecto con los comandos Archivo > Nuevo proyecto . Puede crear un proyecto de Visual
Basic o C#. Cuando Visual Studio abre el principal archivo de código para este proyecto, el visualizador muestra
el árbol de sintaxis correspondiente. Puede abrir cualquier archivo de C# o Visual Basic existente en esta
instancia de Visual Studio y el visualizador mostrará el árbol de sintaxis de ese archivo. Si tiene varios archivos
de código abiertos dentro de Visual Studio, el visualizador muestra el árbol de sintaxis para el archivo de código
activo (el archivo de código que tiene el foco de teclado).
C#
Visual Basic
Como se muestra en las imágenes anteriores, la ventana de herramientas del visualizador muestra el árbol de
sintaxis en la parte superior y una cuadrícula de propiedades en la parte inferior. La cuadrícula de propiedades
muestra las propiedades del elemento que está seleccionado actualmente en el árbol, incluido el Tipo de .NET y
la Variante (SyntaxKind) del elemento.
Los árboles de sintaxis incluyen tres tipos de elementos: nodos, tokens y curiosidades. Encontrará más
información sobre estos tipos en el artículo Trabajar con sintaxis. Los elementos de cada tipo se representan
mediante un color diferente. Haga clic en el botón “Leyenda” para saber más sobre los colores usados.
Cada elemento del árbol también muestra su propio inter valo . El inter valo está comprendido por los índices
(la posición inicial y la final) de ese nodo en el archivo de texto. En el anterior ejemplo de C#, el token
“UsingKeyword [0..5)” seleccionado tiene un inter valo de cinco caracteres de ancho [0..5). La notación “[.)”
significa que el índice inicial forma parte del intervalo, pero el índice final no.
Se puede navegar por el árbol de dos maneras:
Expandir el árbol o hacer clic en él. El visualizador selecciona automáticamente el texto correspondiente al
intervalo de este elemento en el editor de código.
Hacer clic en el texto o seleccionarlo en el editor de código. En el ejemplo de Visual Basic anterior, si
selecciona la línea que contiene "Module Module1" en el editor de código, el visualizador navega
automáticamente al nodo ModuleStatement correspondiente en el árbol.
El visualizador resalta el elemento en el árbol cuyo intervalo coincide mejor con el intervalo del texto
seleccionado en el editor.
El visualizador actualiza el árbol para coincidir con las modificaciones en el archivo de código activo. Agregue
una llamada a Console.WriteLine() dentro de Main() . A medida que escribe, el visualizador actualiza el árbol.
Pare de escribir en cuanto escriba Console. . Verá que el árbol ha marcado en rosa algunos elementos. En este
momento, hay errores (también denominados “diagnósticos”) en el código escrito. Estos errores se adjuntan a
los nodos, los tokens y las curiosidades en el árbol de sintaxis. El visualizador muestra qué elementos tienen
errores adjuntados a ellos resaltando el fondo en color rosa. Puede inspeccionar los errores en cualquier
elemento marcado en rosa si desplaza el puntero sobre el elemento. El visualizador muestra solo los errores
sintácticos (los errores relacionados con la sintaxis del código escrito) y no muestra los errores semánticos.
Gráficos de sintaxis
Haga clic con el botón derecho en cualquier elemento del árbol y haga clic en View Directed Syntax Graph
(Ver gráfico de sintaxis dirigido).
C#
Visual Basic
El visualizador muestra una representación gráfica del subárbol cuya raíz se encuentra en el elemento
seleccionado. Siga estos pasos para el nodo MethodDeclaration correspondiente al método Main() en el
ejemplo de C#. El visualizador muestra un gráfico de sintaxis que tiene este aspecto:
El visor de gráficos de sintaxis tiene una opción para mostrar una leyenda para su esquema de color. También
puede pasar el puntero sobre determinados elementos en el gráfico de sintaxis para ver las propiedades
correspondientes a ese elemento.
Puede ver gráficos de sintaxis de elementos diferentes en el árbol de manera repetida y los gráficos siempre se
mostrarán en la misma ventana dentro de Visual Studio. Puede acoplar esta ventana en una ubicación cómoda
en Visual Studio para no tener que cambiar entre pestañas para ver un nuevo gráfico de sintaxis. La parte
inferior, debajo de las ventanas del editor de código, suele resultar cómoda.
Este es el diseño de acoplamiento que se usa con la ventana del visualizador y la ventana del gráfico de sintaxis:
Otra opción consiste en colocar la ventana del gráfico de sintaxis en un segundo monitor, en una configuración
de monitor dual.
Semántica de inspección
El Visualizador de sintaxis permite realizar una inspección rudimentaria de símbolos e información semántica.
Escriba double x = 1 + 1; dentro de Main() en el ejemplo de C#. Después, seleccione la expresión 1 + 1 en la
ventana del editor de código. El visualizador resalta el nodo AddExpression en el visualizador. Haga clic con el
botón derecho en AddExpression y elija View Symbol (if any) [Ver símbolo (si existe)]. Tenga en cuenta que
la mayoría de los elementos de menú tienen el calificador "si existe". El Visualizador de sintaxis inspecciona las
propiedades de un nodo, incluidas las propiedades que es posible que no estén presentes para todos los nodos.
La cuadrícula de propiedades del visualizador se actualiza tal como se muestra en la figura siguiente: El símbolo
de la expresión es un símbolo SynthesizedIntrinsicOperatorSymbol con Kind = Method .
Intente View TypeSymbol (if any) [Ver TypeSymbol (si existe)] para el mismo nodo AddExpression . La
cuadrícula de propiedades del visualizador se actualiza como se muestra en esta imagen, que indica que el tipo
de la expresión seleccionada es Int32 .
Intente View Conver ted TypeSymbol (if any) [Ver TypeSymbol convertido (si existe)] para el mismo nodo
AddExpression . La cuadrícula de propiedades se actualiza para indicar que, aunque el tipo de la expresión es
Int32 , el tipo convertido de la expresión es Double , como se muestra en esta imagen. Este nodo incluye
información de símbolo de tipo convertido porque la expresión Int32 se produce en un contexto donde se
debe convertir a Double . Esta conversión satisface el tipo Double especificado para la variable x en el lado
izquierdo del operador de asignación.
Por último, intente View Constant Value (if any) [Ver valor de constante (si existe)] para el mismo nodo
AddExpression . La cuadrícula de propiedades muestra que el valor de la expresión es una constante en tiempo
de compilación con el valor 2 .
El ejemplo anterior también se puede replicar en Visual Basic. Escriba Dim x As Double = 1 + 1 en un archivo de
Visual Basic. Seleccione la expresión 1 + 1 en la ventana del editor de código. El visualizador resalta el nodo
AddExpression correspondiente en el visualizador. Repita los pasos anteriores para AddExpression y
deberían mostrarse resultados idénticos.
Examine más código en Visual Basic. Actualice el archivo principal de Visual Basic con este código:
Imports C = System.Console
Module Program
Sub Main(args As String())
C.WriteLine()
End Sub
End Module
Este código incluye un alias llamado C que se asigna al tipo System.Console en la parte superior del archivo y
usa este alias en Main() . Seleccione el uso de este alias, C en C.WriteLine() , dentro del método Main() . El
visualizador selecciona el nodo IdentifierName correspondiente en el visualizador. Haga clic con el botón
derecho en este nodo y elija View Symbol (if any) [Ver símbolo (si existe)]. La cuadrícula de propiedades
indica que este identificador está enlazado al tipo System.Console tal como se muestra en esta imagen:
Intente View AliasSymbol (if any) [Ver AliasSymbol (si existe)] para el mismo nodo IdentifierName . La
cuadrícula de propiedades indica que el identificador es un alias con el nombre C que está enlazado al destino
System.Console . En otras palabras, la cuadrícula de propiedades proporciona información sobre el
AliasSymbol correspondiente al identificador C .
Inspeccione el símbolo correspondiente a cualquier tipo, método o propiedad declarados. Seleccione el nodo
correspondiente en el visualizador y haga clic en View Symbol (if any) [Ver símbolo (si existe)]. Seleccione el
método Sub Main() , incluido el cuerpo del método. Haga clic en View Symbol (if any) [Ver símbolo (si existe)]
para el nodo SubBlock correspondiente en el visualizador. La cuadrícula de propiedades muestra que
MethodSymbol para este nodo SubBlock tiene el nombre Main con el tipo de valor devuelto Void .
Los ejemplos de Visual Basic anteriores se pueden replicar fácilmente en C#. Escriba using C = System.Console;
en lugar de Imports C = System.Console para el alias. Los pasos anteriores en C# producen resultados idénticos
en la ventana del visualizador.
Las operaciones de inspección semántica solo están disponibles en los nodos. No están disponibles en tokens o
curiosidades. No todos los nodos tienen información semántica interesante que inspeccionar. Cuando un nodo
no tiene información semántica interesante, al hacer clic en View * Symbol (if any) [Ver símbolo (si existe)] se
muestra una cuadrícula de propiedades en blanco.
Puede leer más sobre las API para realizar análisis semánticos en el documento introductorio Trabajar con
semántica.
En este artículo se ofrece información general sobre los generadores de código fuente que se incluye como
parte del SDK de .NET Compiler Platform ("Roslyn"). Los generadores de código fuente son una característica del
compilador de C# que permite a los desarrolladores de C# inspeccionar el código de usuario mientras se
compilan y generan nuevos archivos de código fuente de C# sobre la marcha que se agregan a la compilación
del usuario.
Un generador de código fuente es un fragmento de código que se ejecuta durante la compilación y puede
inspeccionar el programa para generar archivos de código fuente adicionales que se compilan junto con el resto
del código.
Un generador de código fuente es un nuevo tipo de componente que los desarrolladores de C# pueden escribir
y que le permite hacer dos cosas principales:
1. Recuperar un objeto de compilación que representa todo el código de usuario que se está compilando.
Este objeto se puede inspeccionar, y puede escribir código que funcione con la sintaxis y los modelos
semánticos del código que se compila, al igual que con los analizadores de hoy en día.
2. Genere archivos de código fuente de C# que se puedan agregar a un objeto de compilación durante el
transcurso de la compilación. En otras palabras, puede proporcionar código fuente adicional como una
entrada en una compilación mientras se compila el código.
Cuando se combinan, estas dos cosas son las que hacen que los generadores de código fuente sean tan útiles.
Puede inspeccionar el código de usuario con todos los metadatos enriquecidos que el compilador crea durante
la compilación y, a continuación, emitir código de C# en la misma compilación que se basa en los datos que ha
analizado. Si está familiarizado con los analizadores de Roslyn, puede pensar en los generadores de código
fuente como analizadores que pueden emitir código fuente de C#.
Los generadores de código fuente se ejecutan como una fase de compilación que se visualiza a continuación:
Un generador de código fuente es un ensamblado de .NET Standard 2.0 que el compilador carga junto con
cualquier analizador. Se puede usar en entornos en los que se pueden cargar y ejecutar los componentes de
.NET Standard.
Escenarios frecuentes
En la actualidad, hay tres enfoques generales para inspeccionar el código de usuario y generar información o
código basado en ese análisis que usan las tecnologías de hoy en día: reflexión en tiempo de ejecución,
entrelazado de IL y obviar las tareas de MSBuild. Los generadores de código fuente pueden mejorar cada
enfoque. La reflexión en tiempo de ejecución es una tecnología eficaz que se agregó a .NET hace mucho tiempo.
Existen incontables escenarios para usarla. Un escenario muy común es realizar algún análisis del código de
usuario cuando se inicia una aplicación y usar los datos para generar elementos.
Por ejemplo, ASP.NET Core usa la reflexión cuando el servicio web se ejecuta por primera vez para detectar las
construcciones que ha definido para que pueda "conectar" elementos, como controladores y Razor Pages.
Aunque esto le permite escribir código sencillo con abstracciones eficaces, incluye una penalización de
rendimiento en tiempo de ejecución: cuando el servicio web o la aplicación se inician por primera vez, no puede
aceptar ninguna solicitud hasta que todo el código de reflexión en tiempo de ejecución que detecta información
sobre el código termine de ejecutarse. Aunque esta penalización de rendimiento no es enorme, es un costo fijo
que no puede mejorar en su propia aplicación.
Con un generador de código fuente, la fase de detección del controlador de inicio podría producirse en tiempo
de compilación mediante el análisis del código fuente y la emisión del código que necesita para "conectar" la
aplicación. Esto podría dar lugar a unos tiempos de inicio más rápidos, ya que una acción que se está
produciendo en tiempo de ejecución podría insertarse en tiempo de compilación. Los generadores de código
fuente pueden mejorar el rendimiento de formas que no se limitan a la reflexión en tiempo de ejecución para
detectar tipos también. Algunos escenarios implican llamar a la tarea de C# de MSBuild (denominada CSC)
varias veces para que puedan inspeccionar los datos desde una compilación. Como puede imaginar, llamar al
compilador más de una vez afecta al tiempo total que se tarda en compilar la aplicación. Estamos investigando
cómo se pueden usar los generadores de código fuente para obviar la necesidad de realizar tareas de MSBuild
como esta, ya que los generadores de código fuente no solo ofrecen algunas ventajas de rendimiento, sino que
también permiten que las herramientas funcionen en el nivel de abstracción correcto.
Otra funcionalidad que los generadores de código fuente pueden ofrecer es obviar el uso de algunas API "con
tipo de cadena"; por ejemplo, cómo funciona el enrutamiento de ASP.NET Core entre controladores y Razor
Pages. Con un generador de código fuente, el enrutamiento puede estar fuertemente tipado con la generación
de las cadenas necesarias como un detalle en tiempo de compilación. Esto reduciría la cantidad de veces que un
literal de cadena mal escrito genera una solicitud que no llega al controlador correcto.
NOTE
Puede ejecutar este ejemplo tal y como está, pero todavía no ocurrirá nada.
3. A continuación, crearemos un generador de código fuente que rellenará el contenido del método
HelloFrom .
4. Cree un proyecto de biblioteca de .NET Standard con este aspecto:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.9.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.0.0" PrivateAssets="all"
/>
</ItemGroup>
</Project>
5. Modifique o cree un archivo de C# que especifique su propio generador de código fuente de la siguiente
manera:
using Microsoft.CodeAnalysis;
namespace MyGenerator
{
[Generator]
public class MySourceGenerator : ISourceGenerator
{
public void Execute(GeneratorExecutionContext context)
{
// Code generation goes here
}
6. Reemplace el contenido del método Execute para que sea similar al siguiente:
public void Execute(GeneratorExecutionContext context)
{
// find the main method
var mainMethod = context.Compilation.GetEntryPoint(context.CancellationToken);
namespace {mainMethod.ContainingNamespace.Name}
{{
public static partial class {mainMethod.ContainingType.Name}
{{
static partial void HelloFrom(string name)
{{
Console.WriteLine($""Generator says: Hi from '{{name}}'"");
}}
}}
}}
";
// add the source code to the compilation
context.AddSource("generatedSource", source);
}
<!-- Add this as a new ItemGroup, replacing paths and names appropriately -->
<ItemGroup>
<!-- Note that this is not a "normal" ProjectReference.
It needs the additional 'OutputItemType' and 'ReferenceOutputAssembly' attributes. -->
<ProjectReference Include="path-to-sourcegenerator-project.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
8. Ahora, al ejecutar la aplicación de consola, debería ver que el código generado se ejecuta e imprime en la
pantalla.
NOTE
Actualmente, tendrá que reiniciar Visual Studio para ver IntelliSense y eliminar los errores con la experiencia de
herramientas inicial.
Pasos siguientes
En la guía paso a paso de los generadores de código fuente se abordan algunos de estos ejemplos con algunos
enfoques recomendados para resolverlos. Además, tenemos un conjunto de ejemplos disponibles en GitHub
que puede probar por su cuenta.
Puede obtener más información sobre los generadores de código fuente en estos temas:
Documento de diseño de los generadores de código fuente
Guía paso a paso de los generadores de código fuente
Introducción al análisis de sintaxis
16/09/2021 • 14 minutes to read
En este tutorial, explorará la API de sintaxis . La API de sintaxis proporciona acceso a las estructuras de datos
que describen un programa de C# o Visual Basic. Estas estructuras de datos tienen suficientes detalles para
representar completamente un programa de cualquier tamaño. Estas estructuras pueden describir programas
completos que se compilen y ejecuten correctamente. También pueden describir programas incompletos,
conforme los escribe, en el editor.
Para habilitar esta expresión completa, las estructuras de datos y las API que constituyen la API de sintaxis son
necesariamente complejas. Empecemos con el aspecto de la estructura de datos para el programa típico “Hola
mundo”:
using System;
using System.Collections.Generic;
using System.Linq;
namespace HelloWorld
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}
Mire el texto del programa anterior. Reconoce elementos conocidos. Todo el texto representa un único archivo
de código fuente o una unidad de compilación . Las tres primeras líneas del archivo de código fuente son
directivas using . El código fuente restante se encuentra en una declaración de espacio de nombres . La
declaración de espacio de nombres contiene una declaración de clase secundaria. La declaración de clase
contiene una declaración de método .
La API de sintaxis crea una estructura de árbol y la raíz representa la unidad de compilación. Los nodos del árbol
representan las directivas using, la declaración del espacio de nombres y todos los demás elementos del
programa. La estructura de árbol continúa hasta los niveles inferiores: la cadena "Hello World!" es un token de
literal de cadena que es un descendiente de un argumento . La API de sintaxis proporciona acceso a la
estructura del programa. Puede consultar los procedimientos de código concretos, recorrer el árbol completo
para entender el código y crear árboles al modificar el árbol existente.
Esa descripción breve proporciona información general sobre el tipo de información accesible mediante la API
de sintaxis. La API de sintaxis no es nada más que una API formal que describe las construcciones de código que
ya conoce de C#. Entre las funcionalidades completas, se incluye información sobre cómo se da formato al
código, incluidos los saltos de línea, los espacios en blanco y la sangría. Con esta información, puede representar
por completo el código tal y como lo escriben y leen los programadores humanos o el compilador. Con esta
estructura, puede interactuar con el código fuente de forma muy significativa. Ya no son cadenas de texto, sino
datos que representan la estructura de un programa de C#.
Para empezar, debe instalar el SDK de .NET Compiler Platform :
Si se desplaza por esta estructura de árbol, podrá encontrar cualquier instrucción, expresión, token o bit de
espacio en blanco en un archivo de código.
Aunque puede buscar cualquier elemento en un archivo de código mediante las API de sintaxis, la mayoría de
los escenarios implican examinar pequeños fragmentos de código o buscar instrucciones o fragmentos
concretos. Los dos ejemplos siguientes muestran usos típicos para examinar la estructura del código o buscar
instrucciones únicas.
Recorrer árboles
Puede examinar los nodos de un árbol de sintaxis de dos maneras. Puede recorrer el árbol para examinar cada
nodo o puede consultar elementos o nodos concretos.
Recorrido manual
Puede ver el código terminado de este ejemplo en nuestro repositorio de GitHub.
NOTE
Los tipos de árbol de sintaxis usan la herencia para describir los diferentes elementos de sintaxis que son válidos en
diferentes ubicaciones del programa. A menudo, usar estas API significa convertir propiedades o miembros de colección
en tipos derivados concretos. En los ejemplos siguientes, la asignación y las conversiones son instrucciones
independientes, con variables con tipo explícito. Puede leer el código para ver los tipos de valor devuelto de la API y el
tipo de motor de ejecución de los objetos devueltos. En la práctica, es más habitual usar variables con tipo implícito y
basarse en nombres de API para describir el tipo de los objetos que se examinan.
Cree un proyecto de Stand-Alone Code Analysis Tool (Herramienta de análisis de código independiente) de
C#:
En Visual Studio, elija Archivo > Nuevo > Proyecto para mostrar el cuadro de diálogo Nuevo proyecto.
En Visual C# > Extensibilidad , elija Stand-Alone Code Analysis Tool (Herramienta de análisis de
código independiente).
Asigne al proyecto el nombre "SyntaxTreeManualTraversal " y haga clic en Aceptar.
Va a analizar el programa básico "Hola mundo" mostrado anteriormente. Agregue el texto para el programa
Hola mundo como una constante en su clase Program :
namespace HelloWorld
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine(""Hello, World!"");
}
}
}";
A continuación, agregue el código siguiente para crear el árbol de sintaxis para el texto del código de la
constante programText . Agregue la línea siguiente al método Main :
Estas dos líneas crean el árbol y recuperan su nodo raíz. Ahora puede examinar los nodos del árbol. Agregue
estas líneas al método Main para mostrar algunas de las propiedades del nodo raíz en el árbol:
WriteLine($"The tree is a {root.Kind()} node.");
WriteLine($"The tree has {root.Members.Count} elements in it.");
WriteLine($"The tree has {root.Usings.Count} using statements. They are:");
foreach (UsingDirectiveSyntax element in root.Usings)
WriteLine($"\t{element.Name}");
Ejecute la aplicación para ver lo que ha detectado el código sobre el nodo raíz de este árbol.
Normalmente, recorrería el árbol para obtener información sobre el código. En este ejemplo, analiza código que
conoce para explorar las API. Agregue el código siguiente para examinar el primer miembro del nodo root :
El nodo de declaración de método contiene toda la información sintáctica sobre el método. Vamos a mostrar el
tipo de valor devuelto del método Main , el número y los tipos de los argumentos, y el texto del cuerpo del
método. Agregue el código siguiente:
Ejecute el programa para ver toda la información que ya conoce sobre este programa:
The tree is a CompilationUnit node.
The tree has 1 elements in it.
The tree has 4 using statements. They are:
System
System.Collections
System.Linq
System.Text
The first member is a NamespaceDeclaration.
There are 1 members declared in this namespace.
The first member is a ClassDeclaration.
There are 1 members declared in the Program class.
The first member is a MethodDeclaration.
The return type of the Main method is void.
The method has 1 parameters.
The type of the args parameter is string[].
The body text of the Main method follows:
{
Console.WriteLine("Hello, World!");
}
Métodos de consulta
Además de recorrer árboles, también puede explorar el árbol de sintaxis mediante los métodos de consulta
definidos en Microsoft.CodeAnalysis.SyntaxNode. Cualquier persona que conozca XPath debería conocer estos
métodos. Puede usarlos con LINQ para buscar elementos rápidamente en un árbol. SyntaxNode tiene métodos
de consulta como DescendantNodes, AncestorsAndSelf y ChildNodes.
Puede usar estos métodos de consulta para buscar el argumento para el método Main como una alternativa a
navegar por el árbol. Agregue el siguiente código en la parte inferior del método Main :
WriteLine(argsParameter == argsParameter2);
La primera instrucción usa una expresión LINQ y el método DescendantNodes para buscar el mismo parámetro
que en el ejemplo anterior.
Ejecute el programa y compruebe que la expresión LINQ ha encontrado el mismo parámetro que se encuentra
al navegar por el árbol de forma manual.
En el ejemplo, se usan instrucciones WriteLine para mostrar información sobre los árboles de sintaxis
conforme se recorren. Puede obtener mucha más información si ejecuta el programa terminado en el
depurador. Puede examinar más propiedades y métodos que forman parte del árbol de sintaxis creado para el
programa Hola mundo.
Rastreadores de sintaxis
A menudo, quiere buscar todos los nodos de un tipo concreto en un árbol de sintaxis, por ejemplo, todas las
declaraciones de propiedad de un archivo. Si extiende la clase
Microsoft.CodeAnalysis.CSharp.CSharpSyntaxWalker e invalida el método
VisitPropertyDeclaration(PropertyDeclarationSyntax), se procesan todas las declaraciones de propiedad de un
árbol de sintaxis sin conocer su estructura de antemano. CSharpSyntaxWalker es un tipo determinado de
CSharpSyntaxVisitor que visita de forma recurrente un nodo y todos sus elementos secundarios.
En este ejemplo, se implementa un CSharpSyntaxWalker que examina un árbol de sintaxis. Recopila directivas
using que determina que no implementan un espacio de nombres System .
Cree un proyecto de Stand-Alone Code Analysis Tool (Herramienta de análisis de código independiente) de
C# y asígnele el nombre “SyntaxWalker ”.
Puede ver el código terminado de este ejemplo en nuestro repositorio de GitHub. El ejemplo de GitHub contiene
los dos proyectos que se describen en este tutorial.
Como en el ejemplo anterior, puede definir una constante de cadena para que contenga el texto del programa
que se va a analizar:
namespace TopLevel
{
using Microsoft;
using System.ComponentModel;
namespace Child1
{
using Microsoft.Win32;
using System.Runtime.InteropServices;
class Foo { }
}
namespace Child2
{
using System.CodeDom;
using Microsoft.CSharp;
class Bar { }
}
}";
Este texto de origen contiene directivas using dispersas por cuatro ubicaciones diferentes: el nivel de archivo,
en el espacio de nombres de nivel superior y en los dos espacios de nombres anidados. En este ejemplo, se
destaca un escenario principal para usar la clase CSharpSyntaxWalker en el código de la consulta. Sería
complejo visitar todos los nodos del árbol de sintaxis raíz para buscar las declaraciones using. En su lugar, cree
una clase derivada y reemplace el método al que se llama solo cuando el nodo actual del árbol sea una directiva
using. El visitante no hace ningún trabajo en ningún otro tipo de nodo. Este método único examina todas las
instrucciones using y compila una colección de los espacios de nombres que no están en el espacio de
nombres System . Compile un CSharpSyntaxWalker que examine todas las instrucciones using , pero solo las
instrucciones using .
Ahora que ha definido el texto del programa, debe crear un SyntaxTree y obtener la raíz de ese árbol:
A continuación, cree una clase. En Visual Studio, elija Proyecto > Agregar nuevo elemento . En el cuadro de
diálogo Agregar nuevo elemento , escriba UsingCollector.cs como nombre de archivo.
Implemente la funcionalidad del visitante using en la clase UsingCollector . Para empezar, haga que la clase
UsingCollector derive de CSharpSyntaxWalker.
Necesita almacenamiento para contener los nodos del espacio de nombres que está recopilando. Declare una
propiedad pública de solo lectura en la clase UsingCollector ; use esta variable para almacenar los nodos
UsingDirectiveSyntax que encuentre:
La clase base, CSharpSyntaxWalker, implementa la lógica para visitar todos los nodos del árbol de sintaxis. La
clase derivada reemplaza los métodos llamados por los nodos específicos que le interesan. En este caso, le
interesa cualquier directiva using . Por tanto, debe invalidar el método
VisitUsingDirective(UsingDirectiveSyntax). El único argumento de este método es un objeto
Microsoft.CodeAnalysis.CSharp.Syntax.UsingDirectiveSyntax. Se trata de una ventaja importante de usar los
visitantes: llaman a los métodos invalidados con argumentos que ya se han convertido al tipo de nodo concreto.
La clase Microsoft.CodeAnalysis.CSharp.Syntax.UsingDirectiveSyntax tiene una propiedad Name que almacena
el nombre del espacio de nombres que se va a importar. Es una
Microsoft.CodeAnalysis.CSharp.Syntax.NameSyntax. Agregue el código siguiente en la invalidación
VisitUsingDirective(UsingDirectiveSyntax):
Como con el ejemplo anterior, ha agregado una variedad de instrucciones WriteLine para ayudar a comprender
este método. Puede ver cuándo se llama y qué argumentos se le pasan cada vez.
Por último, debe agregar dos líneas de código para crear el UsingCollector y hacer que visite el nodo raíz y
recopile todas las instrucciones using . A continuación, agregue un bucle foreach para que muestre todas las
instrucciones using que encuentre el recopilador:
¡Enhorabuena! Ha usado la API de sintaxis para buscar tipos concretos de instrucciones y declaraciones de C#
en el código fuente de C#.
Introducción al análisis semántico
16/09/2021 • 9 minutes to read
En este tutorial, se asume que conoce la API de sintaxis. En el artículo Introducción al análisis de sintaxis se
proporciona una introducción suficiente.
En este tutorial, explorará las API de símbolo y enlace . Estas API ofrecen información sobre el significado
semántico de un programa. Le permiten formular y responder preguntas sobre los tipos representados por
cualquier símbolo en el programa.
Deberá instalar el SDK de .NET Compiler Platform :
Consultar símbolos
En este tutorial, volverá a examinar el programa "Hola mundo". En esta ocasión, consultará los símbolos del
programa para comprender qué tipos representan esos símbolos. Consultará los tipos en un espacio de
nombres y aprenderá a buscar los métodos disponibles en un tipo.
Puede ver el código terminado de este ejemplo en nuestro repositorio de GitHub.
NOTE
Los tipos de árbol de sintaxis usan la herencia para describir los diferentes elementos de sintaxis que son válidos en
diferentes ubicaciones del programa. A menudo, usar estas API significa convertir propiedades o miembros de colección
en tipos derivados concretos. En los ejemplos siguientes, la asignación y las conversiones son instrucciones
independientes, con variables con tipo explícito. Puede leer el código para ver los tipos de valor devuelto de la API y el
tipo de motor de ejecución de los objetos devueltos. En la práctica, es más habitual usar variables con tipo implícito y
basarse en nombres de API para describir el tipo de los objetos que se examinan.
Cree un proyecto de Stand-Alone Code Analysis Tool (Herramienta de análisis de código independiente) de
C#:
En Visual Studio, elija Archivo > Nuevo > Proyecto para mostrar el cuadro de diálogo Nuevo proyecto.
En Visual C# > Extensibilidad , elija Stand-Alone Code Analysis Tool (Herramienta de análisis de
código independiente).
Asigne al proyecto el nombre "SemanticQuickStar t " y haga clic en Aceptar.
Va a analizar el programa básico "Hola mundo" mostrado anteriormente. Agregue el texto para el programa
Hola mundo como una constante en su clase Program :
const string programText =
@"using System;
using System.Collections.Generic;
using System.Text;
namespace HelloWorld
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine(""Hello, World!"");
}
}
}";
A continuación, agregue el código siguiente para crear el árbol de sintaxis para el texto del código de la
constante programText . Agregue la línea siguiente al método Main :
A continuación, compile una CSharpCompilation del árbol que ya ha creado. El ejemplo "Hola mundo" se basa
en los tipos String y Console. Debe hacer referencia al ensamblado que declara esos dos tipos en la compilación.
Agregue la siguiente línea a su método Main para crear una compilación de su árbol de sintaxis, incluida la
referencia al ensamblado adecuado:
Enlazar un nombre
La Compilation crea el SemanticModel desde el SyntaxTree. Después de crear el modelo, puede consultarlo para
buscar la primera directiva using y recuperar la información de símbolo del espacio de nombres System .
Agregue estas dos líneas en su método Main para crear el modelo semántico y recuperar el símbolo de la
primera instrucción using:
// Use the syntax tree to find "using System;"
UsingDirectiveSyntax usingSystem = root.Usings[0];
NameSyntax systemName = usingSystem.Name;
En el código anterior se muestra cómo enlazar el nombre de la primera directiva de using para recuperar un
Microsoft.CodeAnalysis.SymbolInfo para el espacio de nombres System . El código anterior también muestra
que usa el modelo de sintaxis para buscar la estructura del código y usa el modelo semántico para
entender su significado. El modelo de sintaxis busca la cadena System en la instrucción using. El modelo
semántico tiene toda la información sobre los tipos definidos en el espacio de nombres System .
Desde el objeto SymbolInfo puede obtener el Microsoft.CodeAnalysis.ISymbol mediante la propiedad
SymbolInfo.Symbol. Esta propiedad devuelve el símbolo al que hace referencia esta expresión. Para las
expresiones que no hacen referencia a ningún elemento (por ejemplo, los literales numéricos), esta propiedad es
null . Cuando el SymbolInfo.Symbol no es NULL, el ISymbol.Kind denota el tipo del símbolo. En este ejemplo, la
propiedad ISymbol.Kind es un SymbolKind.Namespace. Agregue el código siguiente al método Main . Recupera
el símbolo del espacio de nombres System y, después, muestra todos los espacios de nombres secundarios que
se declaran en el espacio de nombres System :
System.Collections
System.Configuration
System.Deployment
System.Diagnostics
System.Globalization
System.IO
System.Numerics
System.Reflection
System.Resources
System.Runtime
System.Security
System.StubHelpers
System.Text
System.Threading
Press any key to continue . . .
NOTE
La salida no incluye todos los espacios de nombres que son secundarios del espacio de nombres System . Muestra cada
espacio de nombres que se encuentra en esta compilación, que solo hace referencia al ensamblado donde se declara
System.String . Esta compilación no conoce ningún espacio de nombres declarado en otros ensamblados.
Para finalizar este tutorial, crearemos una consulta LINQ que crea una secuencia de todos los métodos públicos
declarados en el tipo string que devuelven una string . Esta consulta es más compleja, así que la
compilaremos línea a línea y, después, la volveremos a construir como una única consulta. El origen de esta
consulta es la secuencia de todos los miembros declarados en el tipo string :
Esa secuencia de origen contiene todos los miembros, incluidas las propiedades y los campos, de modo que
tiene que filtrarlos con el método ImmutableArray<T>.OfType para buscar los elementos que son objetos
Microsoft.CodeAnalysis.IMethodSymbol:
A continuación, agregue otro filtro para devolver solo aquellos métodos que son públicos y devuelven una
string :
Seleccione solo la propiedad name y solo los nombres distintos; para ello, elimine las sobrecargas:
También puede compilar la consulta completa con la sintaxis de consulta LINQ y, después, mostrar todos los
nombres de método en la consola:
foreach (string name in (from method in stringTypeSymbol
.GetMembers().OfType<IMethodSymbol>()
where method.ReturnType.Equals(stringTypeSymbol) &&
method.DeclaredAccessibility == Accessibility.Public
select method.Name).Distinct())
{
Console.WriteLine(name);
}
Join
Substring
Trim
TrimStart
TrimEnd
Normalize
PadLeft
PadRight
ToLower
ToLowerInvariant
ToUpper
ToUpperInvariant
ToString
Insert
Replace
Remove
Format
Copy
Concat
Intern
IsInterned
Press any key to continue . . .
Ha usado la API semántica para buscar y mostrar información sobre los símbolos que forman parte de este
programa.
Introducción a la transformación de sintaxis
16/09/2021 • 13 minutes to read
Este tutorial se basa en conceptos y técnicas explorados en los tutoriales rápidos Introducción al análisis de
sintaxis e Introducción al análisis semántico. Si aún no lo ha hecho, debería completar esos tutoriales antes de
comenzar con este.
En este tutorial rápido, se exploran las técnicas para crear y transformar árboles de sintaxis. En combinación con
las técnicas que aprendió en los tutoriales anteriores, podrá crear la primera refactorización de línea de
comandos.
Creará nodos de sintaxis de nombre para generar el árbol que representa la instrucción
using System.Collections.Generic; . NameSyntax es la clase base para los cuatro tipos de nombres que
aparecen en C#. Junte estos cuatro tipos de nombres para crear cualquier nombre que pueda aparecer en el
lenguaje C#:
Microsoft.CodeAnalysis.CSharp.Syntax.NameSyntax, que representa los nombres de identificador único
simple, como System y Microsoft .
Microsoft.CodeAnalysis.CSharp.Syntax.GenericNameSyntax, que representa un nombre de tipo o método
genérico, como List<int> .
Microsoft.CodeAnalysis.CSharp.Syntax.QualifiedNameSyntax, que representa un nombre completo del
formulario <left-name>.<right-identifier-or-generic-name> , como System.IO .
Microsoft.CodeAnalysis.CSharp.Syntax.AliasQualifiedNameSyntax, que representa un nombre que usa un
alias de ensamblado externo, como LibraryV2::Foo .
Use el método IdentifierName(String) para crear un nodo NameSyntax. Agregue el código siguiente al método
Main en Program.cs :
Vuelva a ejecutar el código y observe los resultados. Está creando un árbol de nodos que representa código.
Continuará con este patrón para compilar QualifiedNameSyntax para el espacio de nombres
System.Collections.Generic . Agrega el código siguiente a Program.cs :
Ejecute el programa de nuevo para ver si ha creado el árbol para agregar el código.
Creación de un árbol modificado
Ha creado un pequeño árbol de sintaxis que contiene una instrucción. Las API para crear nuevos nodos son la
opción correcta para crear instrucciones únicas u otros bloques de código pequeño. Sin embargo, para generar
bloques más grandes de código, debe usar los métodos que reemplazan nodos o insertan nodos en un árbol
existente. Recuerde que los árboles de sintaxis son inmutables. La API de sintaxis no proporciona ningún
mecanismo para modificar un árbol de sintaxis existente después de la construcción. En su lugar, proporciona
métodos que generan nuevos árboles en función de los cambios en los existentes. Se han definido métodos
With* en clases concretas que se derivan de SyntaxNode o en métodos de extensión declarados en la clase
SyntaxNodeExtensions. Estos métodos crean un nuevo nodo al aplicar cambios a las propiedades del elemento
secundario de un nodo existente. Además, el método de extensión ReplaceNode se puede utilizar para
reemplazar un nodo descendiente en un subárbol. Este método también actualiza el elemento primario para que
apunte al formulario secundario recién creado y repite este proceso por todo el árbol: un proceso conocido
como volver a hacer girar el árbol.
El siguiente paso consiste en crear un árbol que representa todo un programa (pequeño) y, a continuación,
modificarlo. Agregue el siguiente código al principio de la clase Program :
namespace HelloWorld
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine(""Hello, World!"");
}
}
}";
NOTE
El código de ejemplo utiliza el espacio de nombres System.Collections y no el espacio de nombres
System.Collections.Generic .
A continuación, agregue el código siguiente a la parte inferior del método Main para analizar el texto y crear un
árbol:
Ejecute el programa y observe con detenimiento el resultado. newUsing todavía no se ha colocado en el árbol
de la raíz. No se ha modificado el árbol original.
Agregue el siguiente código utilizando el método de extensión ReplaceNode para crear un nuevo árbol. El nuevo
árbol es el resultado de reemplazar la importación existente por el nodo newUsing actualizado. Asigne este
nuevo árbol a la variable root existente:
Ejecute el programa otra vez. Esta vez el árbol importa correctamente el espacio de nombres
System.Collections.Generic .
Los métodos With* y ReplaceNode proporcionan un medio cómodo para transformar ramas individuales de un
árbol de sintaxis. La clase Microsoft.CodeAnalysis.CSharp.CSharpSyntaxRewriter realiza varias transformaciones
en un árbol de sintaxis. La clase Microsoft.CodeAnalysis.CSharp.CSharpSyntaxRewriter es una subclase de
Microsoft.CodeAnalysis.CSharp.CSharpSyntaxVisitor<TResult>. CSharpSyntaxRewriter aplica una
transformación a un tipo específico de SyntaxNode. Puede aplicar transformaciones a varios tipos de objetos
SyntaxNode dondequiera que aparezcan en un árbol de sintaxis. En el segundo proyecto de este tutorial rápido
se crea una refactorización de línea de comandos que quita tipos explícitos en declaraciones de variable local en
cualquier lugar en que pudiera utilizarse una inferencia de tipos.
Cree un proyecto de Stand-Alone Code Analysis Tool (Herramienta de análisis de código independiente) de
C#. En Visual Studio, haga clic en el nodo de la solución SyntaxTransformationQuickStart . Elija Agregar >
Nuevo proyecto para mostrar el cuadro de diálogo Nuevo proyecto . En Visual C# > Extensibilidad ,
elija Stand-Alone Code Analysis Tool (Herramienta de análisis de código independiente). Proporcione un
nombre al proyecto TransformationCS y haga clic en Aceptar.
El primer paso es crear una clase que se derive de CSharpSyntaxRewriter para realizar las transformaciones.
Agregue un nuevo archivo de clase al proyecto. En Visual Studio, elija Proyecto > Agregar clase... . En el
cuadro de diálogo Agregar nuevo elemento , escriba TypeInferenceRewriter.cs como nombre de archivo.
Agregue las siguientes directivas using al archivo TypeInferenceRewriter.cs :
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
Agregue el código siguiente para declarar un campo privado de solo lectura para que contenga un
SemanticModel e inicializarlo en el constructor. Necesitará este campo más adelante para determinar donde se
puede usar la inferencia de tipos:
NOTE
Muchas de las API de Roslyn declaran tipos de valor devuelto que son clases base de los tipos en tiempo de ejecución
reales devueltos. En muchos escenarios, un tipo de nodo puede reemplazarse por otro tipo de nodo por completo, e
incluso eliminarse. En este ejemplo, el método VisitLocalDeclarationStatement(LocalDeclarationStatementSyntax) devuelve
un SyntaxNode, en lugar del tipo derivado de LocalDeclarationStatementSyntax. Este sistema de reescritura devuelve un
nuevo nodo LocalDeclarationStatementSyntax basado en el existente.
Este tutorial rápido controla las declaraciones de variable locales. Puede ampliarlo para otras declaraciones
como bucles foreach , bucles for , expresiones LINQ y expresiones lambda. Además, este sistema de
reescritura transformará solo las declaraciones de la forma más sencilla:
Si desea explorar por su cuenta, considere la posibilidad de extender el ejemplo finalizado para estos tipos de
declaraciones de variable:
Agregue el código siguiente al cuerpo del método VisitLocalDeclarationStatement para que omita la reescritura
de estas formas de declaraciones:
if (node.Declaration.Variables.Count > 1)
{
return node;
}
if (node.Declaration.Variables[0].Initializer == null)
{
return node;
}
El método indica que no se realiza la reescritura mediante la devolución del parámetro node sin modificar. Si
ninguna de esas expresiones if son verdaderas, el nodo representa una declaración posible con la
inicialización. Agregue estas instrucciones para extraer el nombre del tipo especificado en la declaración y
asócielo con el campo SemanticModel para obtener un símbolo de tipo:
Por último, agregue la siguiente instrucción if para reemplazar el nombre de tipo existente por la palabra
clave var si el tipo de la expresión de inicializador coincide con el tipo especificado:
if (SymbolEqualityComparer.Default.Equals(variableType, initializerInfo.Type))
{
TypeSyntax varTypeName = SyntaxFactory.IdentifierName("var")
.WithLeadingTrivia(variableTypeName.GetLeadingTrivia())
.WithTrailingTrivia(variableTypeName.GetTrailingTrivia());
El atributo condicional es necesario porque la declaración puede convertir la expresión de inicializador en una
interfaz o clase base. Si se desea, los tipos del lado izquierdo y derecho de la asignación no coinciden. El hecho
de quitar el tipo explícito en estos casos también cambiaría la semántica de un programa. var se especifica
como un identificador en lugar de una palabra clave porque var es una palabra clave contextual. Las
curiosidades iniciales y finales (espacios en blanco) se transfieren desde el nombre de tipo anterior a la palabra
clave var para mantener el espacio en blanco vertical y la sangría. Es más fácil utilizar ReplaceNode en lugar de
With* para transformar el LocalDeclarationStatementSyntax porque el nombre de tipo es realmente el
descendiente del elemento secundario de la instrucción de declaración.
Ha terminado el TypeInferenceRewriter . Ahora vuelva a su archivo Program.cs para finalizar el ejemplo. Cree
una prueba Compilation y obtenga SemanticModel de ella. Use ese SemanticModel para probar su
TypeInferenceRewriter . Llevará a cabo este último paso. Mientras tanto, declare una variable de marcador de
posición que represente la compilación de prueba:
Compilation test = CreateTestCompilation();
Después de un momento de pausa, debería aparecer un subrayado ondulado de error que indica que no existe
ningún método CreateTestCompilation . Presione CTRL+Punto para abrir la bombilla y, a continuación,
presione Entrar para invocar el comando Generar código auxiliar del método . Este comando generará un
código auxiliar para el método CreateTestCompilation en la clase Program . Podrá volver a rellenar este método
más adelante:
Escriba el código siguiente para recorrer en iteración cada SyntaxTree en la prueba Compilation. Para cada una
de ellas, inicialice un nuevo TypeInferenceRewriter con el SemanticModel para ese árbol:
if (newSource != sourceTree.GetRoot())
{
File.WriteAllText(sourceTree.FilePath, newSource.ToFullString());
}
}
Dentro de la instrucción foreach que ha creado, agregue el código siguiente para realizar la transformación en
cada árbol de origen. Este código escribe condicionalmente el nuevo árbol transformado si se hicieron
modificaciones. El sistema de reescritura sólo debe modificar un árbol si encuentra una o más declaraciones de
variables locales que pudieron simplificarse mediante la inferencia de tipos:
if (newSource != sourceTree.GetRoot())
{
File.WriteAllText(sourceTree.FilePath, newSource.ToFullString());
}
Debería ver los subrayados ondulados bajo el código File.WriteAllText . Seleccione la bombilla y agregue la
instrucción using System.IO; necesaria.
Casi ha terminado. Queda un último paso: crear una prueba Compilation. Puesto que no ha utilizado ninguna
inferencia de tipos durante este tutorial rápido, habría sido un caso de prueba perfecto. Desafortunadamente, la
creación de una compilación desde un archivo de proyecto de C# queda fuera del ámbito de este tutorial. Pero
afortunadamente, si ha seguido las instrucciones con cuidado, hay esperanza. Reemplace el contenido del
método CreateTestCompilation con el código siguiente. Crea una compilación de prueba que casualmente
coincide con el proyecto descrito en este tutorial rápido:
MetadataReference mscorlib =
MetadataReference.CreateFromFile(typeof(object).Assembly.Location);
MetadataReference codeAnalysis =
MetadataReference.CreateFromFile(typeof(SyntaxTree).Assembly.Location);
MetadataReference csharpCodeAnalysis =
MetadataReference.CreateFromFile(typeof(CSharpSyntaxTree).Assembly.Location);
return CSharpCompilation.Create("TransformationCS",
sourceTrees,
references,
new CSharpCompilationOptions(OutputKind.ConsoleApplication));
Cruce los dedos y ejecute el proyecto. En Visual Studio, elija Depurar > Iniciar depuración . Visual Studio le
debería notificar que los archivos de su proyecto han cambiado. Haga clic en "Sí a todo " para volver a cargar
los archivos modificados. Examínelos para observar su genialidad. Observe que el código se ve mucho más
limpio sin todos los especificadores de tipo explícitos y redundantes.
¡Enhorabuena! Ha usado las API de compilador para escribir su propia refactorización que busca en todos los
archivos de un proyecto C# ciertos patrones sintácticos, analiza la semántica del código fuente que coincide con
esos patrones y la transforma. ¡Ya es oficialmente un autor de refactorización!
Tutorial: Crear el primer analizador y la corrección
de código
16/09/2021 • 25 minutes to read
El SDK de .NET Compiler Platform proporciona las herramientas que necesita para crear diagnósticos
personalizados (analizadores), correcciones de código, refactorización de código y supresores de diagnóstico
que tengan como destino código de C# o Visual Basic. Un analizador contiene código que reconoce las
infracciones de la regla. La corrección del código contiene el código que corrige la infracción. Las reglas
implementadas pueden ser cualquier elemento de la estructura de código para codificar el estilo de las
convenciones de nomenclatura y mucho más. .NET Compiler Platform proporciona el marco para ejecutar el
análisis a medida que los desarrolladores escriben código y todas las características de la interfaz de usuario de
Visual Studio para corregir código: mostrar líneas de subrayado en el editor, rellenar la lista de errores de Visual
Studio, crear las sugerencias con "bombillas" y mostrar la vista previa enriquecida de las correcciones sugeridas.
En este tutorial, explorará la creación de un analizador y una corrección de código complementaria con el
uso de las API de Roslyn. Un analizador es una manera de realizar análisis de código fuente y notificar un
problema al usuario. Opcionalmente, se puede asociar una corrección de código al analizador para representar
una modificación en el código fuente del usuario. Este tutorial crea un analizador que busca declaraciones de
variable local que podrían declararse mediante el modificador const , aunque no están. La corrección de código
complementaria modifica esas declaraciones para agregar el modificador const .
Requisitos previos
Visual Studio 2019, versión 16.8 o posterior
Debe instalar el SDK de .NET Compiler Platform a través del Instalador de Visual Studio:
Creación de la solución
En Visual Studio, elija Archivo > Nuevo > Proyecto... para mostrar el cuadro de diálogo Nuevo proyecto.
En Visual C# > Extensibilidad , elija Analizador con corrección de código (.NET Standard) .
Asigne al proyecto el nombre "MakeConst " y haga clic en Aceptar.
NOTE
Los analizadores deben tener como destino .NET Standard 2.0 porque se pueden ejecutar en el entorno de .NET Core
(compilaciones de línea de comandos) y el de .NET Framework (Visual Studio).
TIP
Al ejecutar el analizador, inicie una segunda copia de Visual Studio. Esta segunda copia usa un subárbol del registro
diferente para almacenar la configuración. Le permite diferenciar la configuración visual en las dos copias de Visual Studio.
Puede elegir un tema diferente para la ejecución experimental de Visual Studio. Además, no mueva la configuración o el
inicio de sesión a la cuenta de Visual Studio con la ejecución experimental de Visual Studio. Así se mantiene una
configuración diferente.
El subárbol incluye no solo el analizador en desarrollo, sino también los analizadores anteriores abiertos. Para restablecer
el subárbol Roslyn debe eliminarlo manualmente de %LocalAppData%\Microsoft\VisualStudio. El nombre de carpeta del
subárbol Roslyn terminará en Roslyn ; por ejemplo, 16.0_9ae182f9Roslyn . Tenga en cuenta que es posible que tenga
que limpiar la solución y volver a generarla después de eliminar el subárbol.
En la segunda instancia de Visual Studio que acaba de iniciar, cree un proyecto de aplicación de consola de C#
(servirá cualquier marco; los analizadores funcionan en el nivel de origen). Mantenga el mouse sobre el token
con un subrayado ondulado y aparecerá el texto de advertencia proporcionado por un analizador.
La plantilla crea un analizador que notifica una advertencia en cada declaración de tipo, donde el nombre de
tipo contiene letras minúsculas, tal como se muestra en la ilustración siguiente:
La plantilla también proporciona una corrección de código que cambia cualquier nombre de tipo que contiene
caracteres en minúsculas a mayúsculas. Puede hacer clic en la bombilla mostrada con la advertencia para ver los
cambios sugeridos. Al aceptar los cambios sugeridos, se actualizan el nombre de tipo y todas las referencias a
dicho tipo en la solución. Ahora que ha visto el analizador inicial en acción, cierre la segunda instancia de Visual
Studio y vuelva a su proyecto de analizador.
No tiene que iniciar una segunda copia de Visual Studio, y cree código para probar todos los cambios en el
analizador. La plantilla también crea un proyecto de prueba unitaria de forma automática. Este proyecto contiene
dos pruebas. TestMethod1 muestra el formato típico de una prueba que analiza el código sin que se
desencadene un diagnóstico. TestMethod2 muestra el formato de una prueba que desencadena un diagnóstico
y, a continuación, se aplica una corrección de código sugerida. Al crear el analizador y la corrección de código,
deberá escribir pruebas para diferentes estructuras de código para verificar el trabajo. Las pruebas unitarias de
los analizadores son mucho más rápidas que las pruebas interactivas con Visual Studio.
TIP
Las pruebas unitarias del analizador son una herramienta magnífica si se sabe qué construcciones de código deben y no
deben desencadenar el analizador. Cargar el analizador en otra copia de Visual Studio es una herramienta magnífica para
explorar y buscar construcciones en las que puede no haber pensado todavía.
En este tutorial, se escribe un analizador que notifica al usuario las declaraciones de variables locales que se
pueden convertir en constantes locales. Por ejemplo, considere el siguiente código:
int x = 0;
Console.WriteLine(x);
En el código anterior, a x se le asigna un valor constante y nunca se modifica. Se puede declarar con el
modificador const :
const int x = 0;
Console.WriteLine(x);
El análisis para determinar si una variable se puede convertir en constante, que requiere un análisis sintáctico,
un análisis constante de la expresión del inicializador y un análisis del flujo de datos para garantizar que no se
escriba nunca en la variable. .NET Compiler Platform proporciona las API que facilita la realización de este
análisis.
Creación de registros del analizador
La plantilla crea la clase DiagnosticAnalyzer inicial en el archivo MakeConstAnalyzer.cs. Este analizador inicial
muestra dos propiedades importantes de cada analizador.
Cada analizador de diagnóstico debe proporcionar un atributo [DiagnosticAnalyzer] que describe el
lenguaje en el que opera.
Cada analizador de diagnóstico debe derivar (directa o indirectamente) de la clase DiagnosticAnalyzer.
La plantilla también muestra las características básicas que forman parte de cualquier analizador:
1. Registre acciones. Las acciones representan los cambios de código que deben desencadenar el analizador
para examinar el código para las infracciones. Cuando Visual Studio detecta las modificaciones del código
que coinciden con una acción registrada, llama al método registrado del analizador.
2. Cree diagnósticos. Cuando el analizador detecta una infracción, crea un objeto de diagnóstico que Visual
Studio usa para notificar la infracción al usuario.
Registre acciones en la invalidación del método DiagnosticAnalyzer.Initialize(AnalysisContext). En este tutorial,
repasará nodos de sintaxis que buscan declaraciones locales y verá cuáles de ellos tienen valores constantes.
Si una declaración puede ser constante, el analizador creará y notificará un diagnóstico.
El primer paso es actualizar las constantes de registro y el método Initialize , por lo que estas constantes
indican su analizador "Make Const". La mayoría de las constantes de cadena se definen en el archivo de recursos
de cadena. Debe seguir dicha práctica para una localización más sencilla. Abra el archivo Resources.resx para el
proyecto de analizador MakeConst . Muestra el editor de recursos. Actualice los recursos de cadena como sigue:
Cambie AnalyzerDescription a "Variables that are not modified should be made constants.".
Cambie AnalyzerMessageFormat a "Variable '{0}' can be made constant".
Cambie AnalyzerTitle a "Variable can be made constant".
Cuando haya terminado, el editor de recursos debe aparecer como se muestra en la figura siguiente:
Los cambios restantes están en el archivo del analizador. Abra MakeConstAnalyzer.cs en Visual Studio. Cambie la
acción registrada de una que actúa en los símbolos a una que actúa en la sintaxis. En el método
MakeConstAnalyzerAnalyzer.Initialize , busque la línea que registra la acción en los símbolos:
context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType);
context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.LocalDeclarationStatement);
Después de este cambio, puede eliminar el método AnalyzeSymbol . Este analizador examina
SyntaxKind.LocalDeclarationStatement, no las instrucciones SymbolKind.NamedType. Tenga en cuenta que
AnalyzeNode tiene un subrayado ondulado rojo debajo. El código recién agregado hace referencia a un método
AnalyzeNode que no se ha declarado. Declare dicho método con el siguiente código:
private void AnalyzeNode(SyntaxNodeAnalysisContext context)
{
}
int x = 0;
Console.WriteLine(x);
El primer paso es encontrar las declaraciones locales. Agregue el código siguiente a AnalyzeNode en
MakeConstAnalyzer.cs:
Esta conversión siempre se realiza correctamente porque el analizador registró los cambios de las declaraciones
locales, y solo las declaraciones locales. Ningún otro tipo de nodo desencadena una llamada al método
AnalyzeNode . A continuación, compruebe la declaración de cualquier modificador const . Si la encuentra,
devuélvala de inmediato. El código siguiente busca cualquier modificador const en la declaración local:
Por último, deberá comprobar que la variable podría ser const . Esto significa asegurarse de que nunca se
asigne después de inicializarse.
Realizará algún análisis semántico con SyntaxNodeAnalysisContext. Use el argumento context para determinar
si la declaración de variable local puede convertirse en const . Una clase
Microsoft.CodeAnalysis.SemanticModel representa toda la información semántica en un solo archivo de origen.
Puede obtener más información en el artículo que trata los modelos semánticos. Deberá usar
Microsoft.CodeAnalysis.SemanticModel para realizar análisis de flujo de datos en la instrucción de declaración
local. A continuación, use los resultados de este análisis de flujo de datos para garantizar que la variable local no
se escriba con un valor nuevo en cualquier otro lugar. Llame al método de extensión GetDeclaredSymbol para
recuperar ILocalSymbol para la variable y compruebe si no está incluido en la colección
DataFlowAnalysis.WrittenOutside del análisis de flujo de datos. Agregue el código siguiente al final del método
AnalyzeNode :
// Perform data flow analysis on the local declaration.
DataFlowAnalysis dataFlowAnalysis = context.SemanticModel.AnalyzeDataFlow(localDeclaration);
// Retrieve the local symbol for each variable in the local declaration
// and ensure that it is not written outside of the data flow analysis region.
VariableDeclaratorSyntax variable = localDeclaration.Declaration.Variables.Single();
ISymbol variableSymbol = context.SemanticModel.GetDeclaredSymbol(variable, context.CancellationToken);
if (dataFlowAnalysis.WrittenOutside.Contains(variableSymbol))
{
return;
}
El código recién agregado garantiza que no se modifique la variable y que se pueda convertir por tanto en
const . Es el momento de generar el diagnóstico. Agregue el código siguiente a la última línea en AnalyzeNode :
context.ReportDiagnostic(Diagnostic.Create(Rule, context.Node.GetLocation(),
localDeclaration.Declaration.Variables.First().Identifier.ValueText));
Puede comprobar el progreso presionando F5 para ejecutar el analizador. Puede cargar la aplicación de consola
que creó anteriormente y después agregar el siguiente código de prueba:
int x = 0;
Console.WriteLine(x);
Debe aparecer la bombilla, y el analizador debe informar de un diagnóstico. Sin embargo, la bombilla todavía
indica que la plantilla generó la corrección de código e indica que se puede convertir en mayúsculas. En la
sección siguiente se explica cómo escribir la corrección de código.
- int x = 0;
+ const int x = 0;
Console.WriteLine(x);
El usuario la elige en la interfaz de usuario de la bombilla del editor, y Visual Studio cambia el código.
Abra el archivo CodeFixResources.resx y cambie CodeFixTitle a "Make constant".
Abra el archivo MakeConstCodeFixProvider.cs agregado por la plantilla. Esta corrección de código ya está
conectada con el identificador de diagnóstico generado por el analizador de diagnóstico, pero aún no
implementa la transformación de código correcta.
Después, elimine el método MakeUppercaseAsync . Ya no se aplica.
Todos los proveedores de corrección de código se derivan de CodeFixProvider. Todas invalidan
CodeFixProvider.RegisterCodeFixesAsync(CodeFixContext) para notificar las correcciones de código disponibles.
En RegisterCodeFixesAsync , cambie el tipo de nodo antecesor que está buscando por
LocalDeclarationStatementSyntax para que coincida con el diagnóstico:
var declaration =
root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<LocalDeclarationStatementSyntax>
().First();
A continuación, cambie la última línea para registrar una corrección de código. La corrección creará un
documento que resulta de agregar el modificador const a una declaración existente:
Observará un subrayado ondulado rojo en el código que acaba de agregar en el símbolo MakeConstAsync .
Agregue una declaración para MakeConstAsync como el código siguiente:
El nuevo método MakeConstAsync transformará la clase Document que representa el archivo de origen del
usuario en una nueva clase Document que ahora contiene una declaración const .
Se crea un token de palabra clave const para insertarlo en la parte delantera de la instrucción de declaración.
Tenga cuidado de quitar primero cualquier curiosidad inicial del primer token de la instrucción de declaración y
adjúntela al token const . Agregue el código siguiente al método MakeConstAsync :
// Insert the const token into the modifiers list, creating a new modifiers list.
SyntaxTokenList newModifiers = trimmedLocal.Modifiers.Insert(0, constToken);
// Produce the new local declaration.
LocalDeclarationStatementSyntax newLocal = trimmedLocal
.WithModifiers(newModifiers)
.WithDeclaration(localDeclaration.Declaration);
Después aplique formato a la nueva declaración para que coincida con las reglas de formato de C#. Aplicar
formato a los cambios para que coincidan con el código existente mejora la experiencia. Agregue la instrucción
siguiente inmediatamente después del código existente:
// Add an annotation to format the new local declaration.
LocalDeclarationStatementSyntax formattedLocal = newLocal.WithAdditionalAnnotations(Formatter.Annotation);
Se requiere un nuevo espacio de nombres para este código. Agregue la siguiente directiva using al principio
del archivo:
using Microsoft.CodeAnalysis.Formatting;
El último paso es realizar la edición. Hay tres pasos para este proceso:
1. Obtenga un identificador para el documento existente.
2. Cree un documento mediante el reemplazo de la declaración existente con la nueva declaración.
3. Devuelva el nuevo documento.
Agregue el código siguiente al final del método MakeConstAsync :
// Replace the old local declaration with the new local declaration.
SyntaxNode oldRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
SyntaxNode newRoot = oldRoot.ReplaceNode(localDeclaration, formattedLocal);
La corrección de código está lista para probarla. Presione F5 para ejecutar el proyecto del analizador en una
segunda instancia de Visual Studio. En la segunda instancia de Visual Studio, cree un proyecto de aplicación de
consola de C# y agregue algunas declaraciones de variable local inicializadas con valores de constante para el
método Main. Observará que se notifican como advertencias de la siguiente forma.
Ha progresado bastante. Hay subrayados ondulados debajo de las declaraciones que pueden convertirse en
const . Pero aún queda trabajo por hacer. Esto funciona bien si agrega const a las declaraciones a partir de i ,
luego j y, por último, k . Sin embargo, si agrega el modificador const en un orden diferente, a partir de k , el
analizador crea errores: k no puede declararse como const , a menos que i y j ya sean const . Tiene que
realizar más análisis para asegurarse de que controla la forma en que las variables pueden declararse e
inicializarse.
TIP
La biblioteca de pruebas admite una sintaxis de marcado especial, que incluye lo siguiente:
[|text|] : indica que se ha notificado un diagnóstico para text . De forma predeterminada, este formulario solo se
puede usar para probar analizadores con exactamente una instancia de DiagnosticDescriptor proporcionada por
DiagnosticAnalyzer.SupportedDiagnostics .
{|ExpectedDiagnosticId:text|} : indica que se ha notificado un diagnóstico para text con Id
ExpectedDiagnosticId .
Reemplace las pruebas de plantilla de la clase MakeConstUnitTest por el método de prueba siguiente:
[TestMethod]
public async Task LocalIntCouldBeConstant_Diagnostic()
{
await VerifyCS.VerifyCodeFixAsync(@"
using System;
class Program
{
static void Main()
{
[|int i = 0;|]
Console.WriteLine(i);
}
}
", @"
using System;
class Program
{
static void Main()
{
const int i = 0;
Console.WriteLine(i);
}
}
");
}
Ejecute esta prueba para asegurarse de que se supera. En Visual Studio, abra el Explorador de pruebas ; para
ello, seleccione Prueba > Windows > Explorador de pruebas . Luego, seleccione Ejecutar todo .
class Program
{
static void Main()
{
int i = 0;
Console.WriteLine(i++);
}
}
");
}
Esta prueba también pasa. Después, agregue métodos de prueba para las condiciones que todavía no ha
controlado:
Declaraciones que ya son const , porque ya son constantes:
[TestMethod]
public async Task VariableIsAlreadyConst_NoDiagnostic()
{
await VerifyCS.VerifyAnalyzerAsync(@"
using System;
class Program
{
static void Main()
{
const int i = 0;
Console.WriteLine(i);
}
}
");
}
Declaraciones que no tienen inicializador, porque no hay ningún valor para usar:
[TestMethod]
public async Task NoInitializer_NoDiagnostic()
{
await VerifyCS.VerifyAnalyzerAsync(@"
using System;
class Program
{
static void Main()
{
int i;
i = 0;
Console.WriteLine(i);
}
}
");
}
Declaraciones donde el inicializador no es una constante, porque no pueden ser constantes en tiempo de
compilación:
[TestMethod]
public async Task InitializerIsNotConstant_NoDiagnostic()
{
await VerifyCS.VerifyAnalyzerAsync(@"
using System;
class Program
{
static void Main()
{
int i = DateTime.Now.DayOfYear;
Console.WriteLine(i);
}
}
");
}
Puede ser incluso más complicado, porque C# admite varias declaraciones como una instrucción. Considere la
siguiente constante de cadena de caso de prueba:
[TestMethod]
public async Task MultipleInitializers_NoDiagnostic()
{
await VerifyCS.VerifyAnalyzerAsync(@"
using System;
class Program
{
static void Main()
{
int i = 0, j = DateTime.Now.DayOfYear;
Console.WriteLine(i);
Console.WriteLine(j);
}
}
");
}
La variable i puede convertirse en constante, pero la variable j no puede. Por tanto, esta instrucción no
puede convertirse en una declaración de constante.
Vuelva a ejecutar las pruebas y, después, observará que estos nuevos casos de prueba generarán errores.
// Retrieve the local symbol for each variable in the local declaration
// and ensure that it is not written outside of the data flow analysis region.
VariableDeclaratorSyntax variable = localDeclaration.Declaration.Variables.Single();
ISymbol variableSymbol = context.SemanticModel.GetDeclaredSymbol(variable, context.CancellationToken);
if (dataFlowAnalysis.WrittenOutside.Contains(variableSymbol))
{
return;
}
// Ensure that all variables in the local declaration have initializers that
// are assigned with constant values.
foreach (VariableDeclaratorSyntax variable in localDeclaration.Declaration.Variables)
{
EqualsValueClauseSyntax initializer = variable.Initializer;
if (initializer == null)
{
return;
}
El primer bucle foreach examina cada declaración de variable con análisis sintácticos La primera comprobación
garantiza que la variable tiene un inicializador. La segunda comprobación garantiza que el inicializador es una
constante. El segundo bucle tiene el análisis semántico original. Las comprobaciones semánticas se encuentran
en un bucle independiente porque afectan más al rendimiento. Vuelva a ejecutar las pruebas y observará que
todas pasan.
class Program
{
static void Main()
{
int x = {|CS0029:""abc""|};
}
}
");
}
Además, los tipos de referencia no se controlan correctamente. El único valor de constante permitido para un
tipo de referencia es null , excepto en este caso de System.String, que admite los literales de cadena. En otras
palabras, const string s = "abc" es legal, pero const object s = "abc" no lo es. Este fragmento de código
comprueba esa condición:
[TestMethod]
public async Task DeclarationIsNotString_NoDiagnostic()
{
await VerifyCS.VerifyAnalyzerAsync(@"
using System;
class Program
{
static void Main()
{
object s = ""abc"";
}
}
");
}
Para ser exhaustivo, debe agregar otra prueba para asegurarse de que puede crear una declaración de constante
para una cadena. El fragmento de código siguiente define el código que genera el diagnóstico y el código
después de haber aplicado la corrección:
[TestMethod]
public async Task StringCouldBeConstant_Diagnostic()
{
await VerifyCS.VerifyCodeFixAsync(@"
using System;
class Program
{
static void Main()
{
[|string s = ""abc"";|]
}
}
", @"
using System;
class Program
{
static void Main()
{
const string s = ""abc"";
}
}
");
}
Por último, si una variable se declara con la palabra clave var , la corrección de código hace una función
incorrecta y genera una declaración const var , que el lenguaje C# no admite. Para corregir este error, la
corrección de código debe reemplazar la palabra clave var por el nombre del tipo deducido:
[TestMethod]
public async Task VarIntDeclarationCouldBeConstant_Diagnostic()
{
await VerifyCS.VerifyCodeFixAsync(@"
using System;
class Program
{
static void Main()
{
[|var item = 4;|]
}
}
", @"
using System;
class Program
{
static void Main()
{
const int item = 4;
}
}
");
}
[TestMethod]
public async Task VarStringDeclarationCouldBeConstant_Diagnostic()
{
await VerifyCS.VerifyCodeFixAsync(@"
using System;
class Program
{
static void Main()
{
[|var item = ""abc"";|]
}
}
", @"
using System;
class Program
{
static void Main()
{
const string item = ""abc"";
}
}
");
}
Afortunadamente, todos los errores anteriores se pueden tratar con las mismas técnicas que acaba de aprender.
Para corregir el primer error, abra primero MakeConstAnalyzer.cs y busque el bucle foreach donde se
comprueban todos los inicializadores de la declaración local para asegurarse de que se les hayan asignado
valores constantes. Inmediatamente antes del primer bucle foreach, llame a
context.SemanticModel.GetTypeInfo() para recuperar información detallada sobre el tipo declarado de la
declaración local:
// Ensure that the initializer value can be converted to the type of the
// local declaration without a user-defined conversion.
Conversion conversion = context.SemanticModel.ClassifyConversion(initializer.Value, variableType);
if (!conversion.Exists || conversion.IsUserDefined)
{
return;
}
El siguiente cambio se basa en el último. Antes de cerrar la llave del primer bucle foreach, agregue el código
siguiente para comprobar el tipo de declaración local cuando la constante es una cadena o null.
// Special cases:
// * If the constant value is a string, the type of the local declaration
// must be System.String.
// * If the constant value is null, the type of the local declaration must
// be a reference type.
if (constantValue.Value is string)
{
if (variableType.SpecialType != SpecialType.System_String)
{
return;
}
}
else if (variableType.IsReferenceType && constantValue.Value != null)
{
return;
}
Debe escribir algo más de código en el proveedor de corrección de código para reemplazar la palabra clave
var por el nombre de tipo correcto. Vuelva a MakeConstCodeFixProvider.cs. El código que se va a agregar
realiza los pasos siguientes:
Compruebe si la declaración es una declaración var y, en su caso:
Cree un tipo para el tipo deducido.
Asegúrese de que la declaración de tipo no es un alias. Si es así, es válido declarar const var .
Asegúrese de que var no es un nombre de tipo en este programa. (Si es así, const var es válido).
Simplificación del nombre de tipo completo
Parece mucho código. Pero no lo es. Reemplace la línea que declara e inicializa newLocal con el código
siguiente. Va inmediatamente después de la inicialización de newModifiers :
// If the type of the declaration is 'var', create a new type name
// for the inferred type.
VariableDeclarationSyntax variableDeclaration = localDeclaration.Declaration;
TypeSyntax variableTypeName = variableDeclaration.Type;
if (variableTypeName.IsVar)
{
SemanticModel semanticModel = await
document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
// Special case: Ensure that 'var' isn't actually an alias to another type
// (e.g. using var = System.String).
IAliasSymbol aliasInfo = semanticModel.GetAliasInfo(variableTypeName, cancellationToken);
if (aliasInfo == null)
{
// Retrieve the type inferred for var.
ITypeSymbol type = semanticModel.GetTypeInfo(variableTypeName, cancellationToken).ConvertedType;
// Special case: Ensure that 'var' isn't actually a type named 'var'.
if (type.Name != "var")
{
// Create a new TypeSyntax for the inferred type. Be careful
// to keep any leading and trailing trivia from the var keyword.
TypeSyntax typeName = SyntaxFactory.ParseTypeName(type.ToDisplayString())
.WithLeadingTrivia(variableTypeName.GetLeadingTrivia())
.WithTrailingTrivia(variableTypeName.GetTrailingTrivia());
using Microsoft.CodeAnalysis.Simplification;
Ejecute las pruebas, y todas deberían pasar. Felicítese por ejecutar el analizador terminado. Presione Ctrl+F5
para ejecutar el proyecto de analizador en una segunda instancia de Visual Studio con la extensión de la versión
preliminar de Roslyn cargada.
En la segunda instancia de Visual Studio, cree un proyecto de aplicación de consola de C# y agregue
int x = "abc"; al método Main. Gracias a la primera corrección de errores, no se debe notificar ninguna
advertencia para esta declaración de variable local (aunque hay un error del compilador según lo esperado).
A continuación, agregue object s = "abc"; al método Main. Debido a la segunda corrección de errores, no
se debe notificar ninguna advertencia.
Por último, agregue otra variable local que usa la palabra clave var . Observará que se notifica una
advertencia y que aparece una sugerencia debajo a la izquierda.
Mueva el símbolo de intercalación del editor sobre el subrayado ondulado y presione Ctrl+.. para mostrar
la corrección de código sugerida. Al seleccionar la corrección de código, tenga en cuenta que la palabra clave
var ahora se trata correctamente.
Después de estos cambios, obtendrá un subrayado ondulado rojo solo en las dos primeras variables. Agregue
const a i y j , y obtendrá una nueva advertencia sobre k porque ahora puede ser const .
¡Enhorabuena! Ha creado su primera extensión de .NET Compiler Platform que realiza un análisis de código
sobre la marcha para detectar un problema y proporciona una solución rápida para corregirlo. Durante el
proceso, ha aprendido muchas de las API de código que forman parte del SDK de .NET Compiler Platform (API
de Roslyn). Puede comprobar su trabajo con el ejemplo completo en nuestro repositorio de ejemplos de GitHub.
Otros recursos
Introducción al análisis de sintaxis
Introducción al análisis semántico
Guía de programación de C#
16/09/2021 • 2 minutes to read
En esta sección se proporciona información detallada sobre las funcionalidades y características claves del
lenguaje C# a las que C# puede acceder a través de .NET.
En la mayor parte de esta sección se supone que ya sabe algo sobre C# y que conoce los conceptos de
programación generales. Si nunca ha programado ni ha trabajado con C#, puede consultar los tutoriales de
introducción a C# o el tutorial de .NET en explorador, que no requieren ningún conocimiento previo de
programación.
Para obtener información sobre determinadas palabras clave, operadores y directivas de preprocesador,
consulte Referencia de C#. Para obtener información sobre la especificación del lenguaje C#, consulte
Especificación del lenguaje C#.
Secciones de programa
Dentro de un programa de C#
Main() y argumentos de la línea de comandos
Secciones de lenguaje
Instrucciones, expresiones y operadores
Tipos
Clases, estructuras y registros
Interfaces
Delegados
Matrices
Cadenas
Propiedades
Indizadores
Eventos
Genéricos
Iteradores
Expresiones de consulta LINQ
Espacios de nombres
Código no seguro y punteros
Comentarios de la documentación XML
Secciones de la plataforma
Dominios de aplicación
Ensamblados de .NET
Atributos
Colecciones
Excepciones y control de excepciones
Registro y sistema de archivos (Guía de programación de C#)
Interoperabilidad
Reflexión
Consulte también
Referencia de C#
Conceptos de programación (C#)
16/09/2021 • 2 minutes to read
En esta sección
T IT L E DESC RIP C IÓ N
Programación asincrónica con Async y Await (C#) Describe cómo escribir soluciones asincrónicas mediante las
palabras clave Async y Await en C#. Incluye un tutorial.
Árboles de expresión (C#) Explica cómo puede utilizar árboles de expresión para
habilitar la modificación dinámica de código ejecutable.
Language Integrated Query (LINQ) (C#) Se describen las eficaces funciones de consulta de la sintaxis
del lenguaje C#, así como el modelo para consultar bases de
datos relacionales, documentos XML, conjuntos de datos y
colecciones en memoria.
Secciones relacionadas
Sugerencias para mejorar el rendimiento
Se describen varias reglas básicas que pueden ayudarle a aumentar el rendimiento de la aplicación.
Programación asincrónica con async y await
16/09/2021 • 17 minutes to read
El modelo de programación asincrónica de tareas (TAP) es una abstracción del código asincrónico. El código se
escribe como una secuencia de instrucciones, como es habitual. Puede leerlo como si cada instrucción se
completase antes de comenzar la siguiente. El compilador realiza diversas transformaciones porque algunas de
estas instrucciones podrían empezar a funcionar y devolver una clase Task que representase el trabajo en curso.
Este es el objetivo de la sintaxis: habilitar código que se lea como una secuencia de instrucciones, pero que se
ejecute siguiendo un orden mucho más complicado, en función de la asignación de recursos externos y del
momento en el que se completen las tareas. Es similar a la manera en la que las personas dan instrucciones para
los procesos que incluyen las tareas asincrónicas. En este artículo, usará un ejemplo con instrucciones para
preparar el desayuno que le ayudará a comprender cómo las palabras clave async y await facilitan el proceso
de razonar sobre el código, que incluye una serie de instrucciones asincrónicas. Para explicar cómo se prepara
un desayuno, probablemente escribirá unas instrucciones parecidas a las que se recogen en la lista siguiente:
1. Sirva una taza de café.
2. Caliente una sartén y fría dos huevos.
3. Fría tres lonchas de beicon.
4. Tueste dos rebanadas de pan.
5. Unte el pan con mantequilla y mermelada.
6. Sirva un vaso de zumo de naranja.
Si tiene experiencia en la cocina, lo más probable es que ejecute estas instrucciones de forma asincrónica .
Primero, calentará la sartén para los huevos e irá friendo el beicon. Después, pondrá el pan en la tostadora y
empezará a freír los huevos. En cada paso del proceso, iniciará una tarea y volverá la atención a las tareas que
tiene pendientes.
La preparación del desayuno es un buen ejemplo de un trabajo asincrónico que no es paralelo. Una persona (o
un subproceso) puede controlar todas estas tareas. Siguiendo con la analogía del desayuno, una persona puede
preparar el desayuno asincrónicamente si comienza la tarea siguiente antes de que finalice la anterior. Los
alimentos se cocinan tanto si una persona supervisa el proceso como si no. En cuanto se empieza a calentar la
sartén para los huevos, se puede comenzar a freír el beicon. Una vez que el beicon se esté haciendo, se puede
poner el pan en la tostadora.
En el caso de un algoritmo paralelo, necesitaría varios cocineros (o subprocesos). Uno se encargaría de los
huevos, otro del beicon, etc. Cada uno de ellos se centraría en una sola tarea. Un cocinero (o subproceso) se
bloqueará al esperar asincrónicamente a que el beicon se dore para darle la vuelta, o al esperar a que las
tostadas estén listas.
Piense ahora en estas mismas instrucciones escritas como instrucciones de C#:
using System;
using System.Threading.Tasks;
namespace AsyncBreakfast
{
class Program
{
static void Main(string[] args)
{
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
Egg eggs = FryEggs(2);
Console.WriteLine("eggs are ready");
Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
}
El desayuno preparado de forma sincrónica tardó unos 30 minutos porque el total es la suma de cada tarea
individual.
NOTE
Las clases Coffee , Egg , Bacon , Toast y Juice están vacías. Simplemente son clases de marcador creadas para la
demostración; no contienen propiedades y no sirven para ningún otro propósito.
Los equipos no interpretan estas instrucciones de la misma manera que las personas. El equipo se bloqueará en
cada instrucción hasta que el trabajo se complete antes de pasar a la instrucción siguiente. Podría decirse que
esto da lugar a un desayuno poco satisfactorio. Las tareas posteriores no se pueden iniciar mientras no se
completen las anteriores. Así pues, se tardará mucho más en preparar el desayuno y algunos alimentos se
habrán enfriado incluso antes de servirse.
Si quiere que el equipo ejecute las instrucciones anteriores de forma asincrónica, debe escribir código
asincrónico.
Estas cuestiones son importantes para los programas que se escriben hoy en día. Al escribir programas cliente,
le interesa que la interfaz de usuario responda a la entrada del usuario. La aplicación no debería hacer que un
teléfono parezca congelado mientras descarga datos de la Web. Al escribir programas de servidor, no le
conviene que los subprocesos se bloqueen. La intención es que puedan atender también otras solicitudes. El uso
de código sincrónico cuando existen alternativas asincrónicas va en detrimento de la capacidad de escalar
horizontalmente a un menor coste. Al final, los subprocesos bloqueados pasarán factura.
Las aplicaciones modernas de más éxito requieren código asincrónico. Sin la compatibilidad con los lenguajes, la
escritura de código asincrónico requería devoluciones de llamada, eventos de finalización u otros medios que
impedían ver claramente la intención original del código. La ventaja del código sincrónico es que las acciones
paso a paso facilitan el análisis y la comprensión. Los modelos asincrónicos tradicionales obligaban a centrarse
en la naturaleza asincrónica del código, no en las acciones fundamentales.
Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
}
IMPORTANT
El tiempo total transcurrido es aproximadamente el mismo que el de la versión inicial sincrónica. El código todavía tiene
que aprovechar algunas de las características clave de la programación asincrónica.
TIP
Los cuerpos del método de FryEggsAsync , FryBaconAsync y ToastBreadAsync se han actualizado para devolver
Task<Egg> , Task<Bacon> y Task<Toast> , respectivamente. Se cambia el nombre de los métodos de su versión
original para incluir el sufijo "Async". Sus implementaciones se muestran como parte de la versión final más adelante en
este artículo.
Este código no produce un bloqueo mientras se cocinan los huevos o el beicon, pero tampoco inicia otras tareas.
Es decir, pondría el pan en la tostadora y se quedaría esperando a que estuviera listo, pero, por lo menos, si
alguien reclamara su atención, le haría caso. En un restaurante en el que se atienden varios pedidos, el cocinero
empezaría a preparar otro desayuno mientras se hace el primero.
El subproceso que se encarga del desayuno ya no se bloquearía mientras espera por las tareas iniciadas que aún
no han terminado. En algunas aplicaciones, lo único que se necesita es este cambio. Una aplicación de interfaz
gráfica de usuario seguirá respondiendo al usuario con este único cambio. Aun así, para este escenario, necesita
algo más. No le interesa que todas las tareas de componente se ejecuten secuencialmente. Es mejor iniciar cada
una de estas tareas sin esperar a que la tarea anterior se complete.
Realicemos estos cambios en el código del desayuno. El primer paso consiste en almacenar las tareas de las
operaciones cuando se inician, en lugar de esperar por ellas:
Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
Después, puede mover las instrucciones await del beicon y los huevos al final del método, antes de servir el
desayuno:
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
Console.WriteLine("Breakfast is ready!");
La preparación del desayuno de forma asincrónica apenas tomó 20 minutos, lo cual supone un ahorro de
tiempo que se debe a que algunas tareas se efectuaron simultáneamente.
El código anterior funciona mejor. Iniciará todas las tareas asincrónicas a la vez y esperará por una tarea solo
cuando necesite los resultados. El código anterior se parece al código de una aplicación web que realiza
solicitudes a diferentes microservicios y, después, combina los resultados en una sola página. Podrá realizar
todas las solicitudes de inmediato y, luego, llevará a cabo una instrucción await para esperar por todas esas
tareas y componer la página web.
En el código anterior se muestra que se puede usar un objeto Task o Task<TResult> para conservar tareas en
ejecución. Lleva a cabo una instrucción await para esperar por una tarea a fin de poder usar su resultado. El
siguiente paso consiste en crear métodos que representan la combinación de otro trabajo. Antes de servir el
desayuno, quiere esperar por la tarea que representa tostar el pan antes de untar la mantequilla y la mermelada.
Puede representar este trabajo con el código siguiente:
return toast;
}
El método anterior tiene el modificador async en su firma, lo que indica al compilador que este método incluye
una instrucción await , es decir, que contiene operaciones asincrónicas. Este método representa la tarea que
tuesta el pan y, después, agrega la mantequilla y la mermelada. El método devuelve un objeto Task<TResult>
que representa la composición de estas tres operaciones. El bloque principal de código se convierte en lo
siguiente:
Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
}
El cambio anterior ilustra una técnica importante para trabajar con código asincrónico. Para componer tareas,
las operaciones se separan en un método nuevo que devuelve una tarea. Usted puede elegir cuándo se debe
esperar por esta tarea y puede iniciar otras tareas simultáneamente.
Excepciones asincrónicas
Hasta este momento, ha asumido implícitamente que todas estas tareas se completan correctamente. Los
métodos asincrónicos generan excepciones, al igual que sus homólogos sincrónicos. La compatibilidad
asincrónica con la administración de excepciones y errores presenta los mismos objetivos en general: escribir
código que se lea como una serie de instrucciones sincrónicas. Las tareas, cuando no se pueden completar
correctamente, generan excepciones. El código cliente puede capturar dichas excepciones cuando una tarea
presenta el elemento awaited . Por ejemplo, supongamos que, al hacer una tostada, la tostadora empieza a
arder. Para simular esta situación, puede modificar el método ToastBreadAsync para que coincida con el código
siguiente:
NOTE
Al compilar el código anterior, recibirá una advertencia referente a código inaccesible. Es algo intencionado, porque, una
vez que la tostadora empiece a arder, la actividad no se podrá llevar a cabo con normalidad.
Ejecute la aplicación tras efectuar dichos cambios, con una salida similar al texto siguiente:
Pouring coffee
coffee is ready
Warming the egg pan...
putting 3 slices of bacon in the pan
cooking first side of bacon...
Putting a slice of bread in the toaster
Putting a slice of bread in the toaster
Start toasting...
Fire! Toast is ruined!
flipping a slice of bacon
flipping a slice of bacon
flipping a slice of bacon
cooking the second side of bacon...
cracking 2 eggs
cooking the eggs ...
Put bacon on plate
Put eggs on plate
eggs are ready
bacon is ready
Unhandled exception. System.InvalidOperationException: The toaster is on fire
at AsyncBreakfast.Program.ToastBreadAsync(Int32 slices) in Program.cs:line 65
at AsyncBreakfast.Program.MakeToastWithButterAndJamAsync(Int32 number) in Program.cs:line 36
at AsyncBreakfast.Program.Main(String[] args) in Program.cs:line 24
at AsyncBreakfast.Program.<Main>(String[] args)
Observe que hay unas cuantas tareas que se completan entre que la tostadora empieza a arder y se genera la
excepción. Cuando una tarea que se ejecuta de forma asincrónica genera una excepción, esta tarea pasa a ser
errónea . El objeto de la tarea contiene la excepción generada en la propiedad Task.Exception. Las tareas
erróneas, al encontrarse en espera, generan una excepción.
Hay dos mecanismos importantes que es necesario entender: el modo en el que una excepción se almacena en
una tarea errónea, y el modo en el que una excepción se desempaqueta y se vuelve a generar cuando el código
espera una tarea errónea.
Si el código se ejecuta de forma asincrónica y genera una excepción, dicha excepción se almacena en Task . La
propiedad Task.Exception es un elemento System.AggregateException porque es posible que se genere más de
una excepción durante un trabajo asincrónico. Toda excepción generada se agrega a la colección
AggregateException.InnerExceptions. Si dicha propiedad Exception es NULL, se crea AggregateException y la
excepción generada es el primer elemento de la colección.
En el caso de una tarea errónea, el escenario más habitual es que la propiedad Exception contenga
exactamente una excepción. Si el código contempla un elemento awaits relativo a una tarea errónea, la primera
excepción de la colección AggregateException.InnerExceptions se vuelve a generar. Ese es el motivo por el que la
salida de este ejemplo muestra un elemento InvalidOperationException , en lugar de AggregateException . El
hecho de extraer la primera excepción interna hace que trabajar con métodos asincrónicos sea lo más similar
posible a trabajar con sus homólogos sincrónicos. Si en su caso se generan varias excepciones, puede examinar
la propiedad Exception del código.
Antes de continuar, comente estas dos líneas de su método ToastBreadAsync . No quiere que arda nada más:
Otra opción consiste en usar WhenAny, que devuelve un objeto Task<Task> que se completa cuando finaliza
cualquiera de sus argumentos. Puede esperar por la tarea devuelta, con la certeza de saber que ya ha terminado.
En el código siguiente se muestra cómo se puede usar WhenAny para esperar a que la primera tarea finalice y,
después, procesar su resultado. Después de procesar el resultado de la tarea completada, quítela de la lista de
tareas que se pasan a WhenAny .
var breakfastTasks = new List<Task> { eggsTask, baconTask, toastTask };
while (breakfastTasks.Count > 0)
{
Task finishedTask = await Task.WhenAny(breakfastTasks);
if (finishedTask == eggsTask)
{
Console.WriteLine("eggs are ready");
}
else if (finishedTask == baconTask)
{
Console.WriteLine("bacon is ready");
}
else if (finishedTask == toastTask)
{
Console.WriteLine("toast is ready");
}
breakfastTasks.Remove(finishedTask);
}
Después de todos estos cambios, la versión final del código tiene un aspecto similar al siguiente:
.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace AsyncBreakfast
{
class Program
{
static async Task Main(string[] args)
{
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
}
return toast;
}
Pasos siguientes
Escenarios del mundo real para programas asincrónicos
Programación asincrónica
16/09/2021 • 12 minutes to read
Si tiene cualquier necesidad enlazada a E/S (por ejemplo, solicitar datos de una red, acceder a una base de datos
o leer y escribir un sistema de archivos), deberá usar la programación asincrónica. También podría tener código
enlazado a la CPU, como realizar un cálculo costoso, que también es un buen escenario para escribir código
asincrónico.
C# tiene un modelo de programación asincrónico de nivel de lenguaje que permite escribir fácilmente código
asincrónico sin tener que hacer malabares con las devoluciones de llamada o ajustarse a una biblioteca que
admita la asincronía. Sigue lo que se conoce como el modelo asincrónico basado en tareas (TAP).
El código expresa la intención (descargar datos de forma asincrónica) sin verse obstaculizado en la interacción
con objetos Task .
Ejemplo enlazado a la CPU: realizar un cálculo para un juego
Supongamos que está escribiendo un juego para móviles en el que se pueden infligir daños a muchos
enemigos en la pantalla pulsando un botón. Realizar el cálculo del daño puede resultar costoso y hacerlo en el
subproceso de interfaz de usuario haría que pareciera que el juego se pone en pausa mientras se lleva a cabo el
cálculo.
La mejor manera de abordar esta situación consiste en iniciar un subproceso en segundo plano que realice la
tarea mediante Task.Run y esperar su resultado mediante await . Esto permite que la interfaz de usuario
funcione de manera fluida mientras se lleva a cabo la tarea.
Este código expresa claramente la intención del evento de clic del botón, no requiere la administración manual
de un subproceso en segundo plano y lo hace en un modo sin bloqueo.
Qué sucede en segundo plano
En las operaciones asincrónicas existen numerosos aspectos dinámicos. Si siente curiosidad sobre lo que ocurre
en el segundo plano de Task y Task<T> , eche un vistazo al artículo Async en profundidad para obtener más
información.
En lo que respecta a C#, el compilador transforma el código en una máquina de estados que realiza el
seguimiento de acciones como la retención de la ejecución cuando se alcanza await y la reanudación de la
ejecución cuando se ha finalizado un trabajo en segundo plano.
Para los más interesados en la teoría, se trata de una implementación del modelo de promesas de asincronía.
Más ejemplos
En los ejemplos siguientes se muestran distintas maneras en las que puede escribir código asincrónico en C#.
Abarcan algunos escenarios diferentes con los que puede encontrarse.
Extracción de datos de una red
Este fragmento de código descarga el HTML desde la página principal en https://dotnetfoundation.org y cuenta
el número de veces que aparece la cadena ".NET" en el código HTML. Usa ASP.NET para definir un método de
controlador Web API que realiza esta tarea y devuelve el número.
NOTE
Si tiene previsto realizar un análisis HTML en el código de producción, no use expresiones regulares. Use una biblioteca de
análisis en su lugar.
[HttpGet, Route("DotNetCount")]
public async Task<int> GetDotNetCount()
{
// Suspends GetDotNetCount() to allow the caller (the web server)
// to accept another request, rather than blocking on this one.
var html = await _httpClient.GetStringAsync("https://dotnetfoundation.org");
Este es el mismo escenario escrito para una aplicación Windows Universal, que realiza la misma tarea cuando se
presiona un botón:
private readonly HttpClient _httpClient = new HttpClient();
// Any other work on the UI thread can be done here, such as enabling a Progress Bar.
// This is important to do here, before the "await" call, so that the user
// sees the progress bar before execution of this method is yielded.
NetworkProgressBar.IsEnabled = true;
NetworkProgressBar.Visibility = Visibility.Visible;
NetworkProgressBar.IsEnabled = false;
NetworkProgressBar.Visibility = Visibility.Collapsed;
}
Aquí tiene otra manera de escribir lo mismo de una forma más sucinta, con LINQ:
public async Task<User> GetUserAsync(int userId)
{
// Code omitted:
//
// Given a user Id {userId}, retrieves a User object corresponding
// to the entry in the database with {userId} as its Id.
}
Aunque es menos código, tenga cuidado al combinar LINQ con código asincrónico. Dado que LINQ usa la
ejecución diferida, las llamadas asincrónicas no se realizarán inmediatamente, como lo hacen en un bucle
foreach , a menos que fuerce la secuencia generada a procesar una iteración con una llamada a .ToList() o
.ToArray() .
Otros recursos
En el artículo Async en profundidad se proporciona más información sobre cómo funcionan las tareas.
Modelo de programación asincrónica de tareas (C#).
Los vídeos Six Essential Tips for Async (Seis consejos esenciales para la programación asincrónica) de Lucian
Wischik son un recurso fantástico para este tipo de programación.
Modelo de programación asincrónica de tareas
16/09/2021 • 14 minutes to read
Puede evitar cuellos de botella de rendimiento y mejorar la capacidad de respuesta total de la aplicación
mediante la programación asincrónica. Sin embargo, las técnicas tradicionales para escribir aplicaciones
asincrónicas pueden resultar complicadas, haciéndolas difícil de escribir, depurar y mantener.
C# 5 ha introducido un enfoque simplificado, la programación asincrónica, que aprovecha la compatibilidad
asincrónica de .NET Framework 4.5 y versiones posteriores, NET Core y Windows Runtime. El compilador realiza
el trabajo difícil que el desarrollador suele realizar y la aplicación conserva una estructura lógica similar al
código sincrónico. Como resultado, se obtienen todas las ventajas de la programación asincrónica con una parte
del trabajo.
Este tema proporciona información general sobre cuándo y cómo utilizar la programación asincrónica e incluye
vínculos que admiten temas con detalles y ejemplos.
T IP O S DE . N ET C O N M ÉTO DO S T IP O S DE W IN DO W S RUN T IM E C O N
Á REA DE A P L IC A C IÓ N A SIN C RÓ N IC O S M ÉTO DO S A SIN C RÓ N IC O S
La asincronía es especialmente valiosa para aquellas aplicaciones que obtienen acceso al subproceso de interfaz
de usuario, ya que todas las actividades relacionadas con la interfaz de usuario normalmente comparten un
único subproceso. Si se bloquea un proceso en una aplicación sincrónica, se bloquean todos. La aplicación deja
de responder y puede que se piense que se ha producido un error cuando en realidad la aplicación está
esperando.
Cuando se usan métodos asincrónicos, la aplicación continúa respondiendo a la interfaz de usuario. Puede
cambiar el tamaño o minimizar una ventana, por ejemplo, o puede cerrar la aplicación si no desea esperar a que
finalice.
El enfoque basado en asincrónico agrega el equivalente de una transmisión automática a la lista de opciones
entre las que puede elegir al diseñar operaciones asincrónicas. Es decir, obtiene todas las ventajas de la
programación asincrónica tradicional pero con mucho menos trabajo de desarrollador.
Task<string> getStringTask =
client.GetStringAsync("https://docs.microsoft.com/dotnet");
DoIndependentWork();
return contents.Length;
}
void DoIndependentWork()
{
Console.WriteLine("Working...");
}
Del ejemplo anterior puede obtener información sobre varios procedimientos. Comience con la firma del
método. Incluye el modificador async . El tipo de valor devuelto es Task<int> (vea la sección "Tipos de valor
devuelto" para obtener más opciones). El nombre del método termina en Async . En el cuerpo del método,
GetStringAsync devuelve un elemento Task<string> . Esto significa que, cuando se aplica await a la tarea, se
obtiene un elemento string ( contents ). Antes de la espera de la tarea, puede realizar otras acciones que no se
basen en el elemento string de GetStringAsync .
Preste mucha atención al operador await . Suspende a GetUrlContentLengthAsync :
GetUrlContentLengthAsync no puede continuar hasta que se complete getStringTask .
Mientras tanto, el control vuelve al autor de la llamada de GetUrlContentLengthAsync .
Aquí se reanuda el control cuando se completa getStringTask .
Después, el operador await recupera el resultado string de getStringTask .
La instrucción return especifica un resultado entero. Los métodos que están a la espera de
GetUrlContentLengthAsync recuperan el valor de longitud.
Si GetUrlContentLengthAsync no hay ningún trabajo que se pueda hacer entre llamar a GetStringAsync y esperar
a su finalización, se puede simplificar el código llamando y esperando en la siguiente instrucción única.
string contents = await client.GetStringAsync("https://docs.microsoft.com/dotnet");
Las siguientes características resumen lo que hace que el ejemplo anterior sea un método asincrónico:
Method Signature incluye un modificador async .
El nombre de un método asincrónico, por convención, finaliza con un sufijo “Async”.
El tipo de valor devuelto es uno de los tipos siguientes:
Task<TResult> si el método tiene una instrucción return en la que el operando tiene el tipo TResult .
Task si el método no tiene ninguna instrucción return ni tiene una instrucción return sin operando.
void si está escribiendo un controlador de eventos asincrónicos.
Cualquier otro tipo que tenga un método GetAwaiter (a partir de C# 7.0).
Para obtener más información, consulte la sección Tipos de valor devuelto y parámetros.
El método normalmente incluye al menos una expresión await , que marca un punto en el que el método
no puede continuar hasta que se completa la operación asincrónica en espera. Mientras tanto, se
suspende el método y el control vuelve al llamador del método. La sección siguiente de este tema
muestra lo que sucede en el punto de suspensión.
En métodos asincrónicos, se utilizan las palabras clave y los tipos proporcionados para indicar lo que se desea
hacer y el compilador realiza el resto, incluido el seguimiento de qué debe ocurrir cuando el control vuelve a un
punto de espera en un método suspendido. Algunos procesos de rutina, tales como bucles y control de
excepciones, pueden ser difíciles de controlar en código asincrónico tradicional. En un método asincrónico, se
pueden escribir estos elementos como se haría en una solución sincrónica y se resuelve este problema.
Para obtener más información sobre la asincronía en versiones anteriores de .NET Framework, vea TPL y la
programación asincrónica tradicional de .NET Framework.
Dentro del método de llamada, el patrón de procesamiento continúa. El llamador puede hacer otro
trabajo que no depende del resultado de GetUrlContentLengthAsync antes de esperar ese resultado, o es
posible que el llamador se espere inmediatamente. El método de llamada espera a
GetUrlContentLengthAsync , y GetUrlContentLengthAsync espera a GetStringAsync .
Subprocesos
La intención de los métodos Async es ser aplicaciones que no pueden producir bloqueos. Una expresión await
en un método asincrónico no bloquea el subproceso actual mientras la tarea esperada se encuentra en
ejecución. En vez de ello, la expresión declara el resto del método como una continuación y devuelve el control
al llamador del método asincrónico.
Las palabras clave async y await no hacen que se creen subprocesos adicionales. Los métodos Async no
requieren multithreading, ya que un método asincrónico no se ejecuta en su propio subproceso. El método se
ejecuta en el contexto de sincronización actual y ocupa tiempo en el subproceso únicamente cuando el método
está activo. Puede utilizar Task.Run para mover el trabajo enlazado a la CPU a un subproceso en segundo plano,
pero un subproceso en segundo plano no ayuda con un proceso que solo está esperando a que los resultados
estén disponibles.
El enfoque basado en asincrónico en la programación asincrónica es preferible a los enfoques existentes en casi
todos los casos. En concreto, este enfoque es mejor que la clase BackgroundWorker para las operaciones
enlazadas a E/S porque el código es más sencillo y no se tiene que proteger contra las condiciones de carrera.
Junto con el método Task.Run, la programación asincrónica es mejor que BackgroundWorker para las
operaciones enlazadas a la CPU, ya que la programación asincrónica separa los detalles de coordinación en la
ejecución del código del trabajo que Task.Run transfiere al grupo de subprocesos.
Async y Await
Si especifica que un método es un método asincrónico mediante el modificador async, habilita las dos funciones
siguientes.
El método asincrónico marcado puede utilizar await para designar puntos de suspensión. El operador
await indica al compilador que el método asincrónico no puede continuar pasado ese punto hasta que
se complete el proceso asincrónico aguardado. Mientras tanto, el control devuelve al llamador del
método asincrónico.
La suspensión de un método asincrónico en una expresión await no constituye una salida del método y
los bloques finally no se ejecutan.
El método asincrónico marcado sí se puede esperar por los métodos que lo llaman.
Un método asincrónico normalmente contiene una o más apariciones de un operador await , pero la ausencia
de expresiones await no causa errores de compilación. Si un método asincrónico no usa un operador await
para marcar el punto de suspensión, se ejecuta como un método sincrónico, a pesar del modificador async . El
compilador detecta una advertencia para dichos métodos.
async y await son palabras clave contextuales. Para mayor información y ejemplos, vea los siguientes temas:
async
await
return hours;
}
Cada tarea devuelta representa el trabajo en curso. Una tarea encapsula la información sobre el estado del
proceso asincrónico y, finalmente, el resultado final del proceso o la excepción que el proceso provoca si no
tiene éxito.
Un método asincrónico también puede tener un tipo de valor devuelto void . Este tipo de valor devuelto se
utiliza principalmente para definir controladores de eventos, donde se requiere un tipo de valor devuelto void .
Los controladores de eventos asincrónicos sirven a menudo como punto de partida para programas
asincrónicos.
No se puede esperar a un método asincrónico que tenga un tipo de valor devuelto void y el llamador de un
método con tipo de valor devuelto void no puede capturar ninguna excepción producida por este.
Un método asincrónico no puede declarar ningún parámetro in, ref o out, pero el método puede llamar a los
métodos que tienen estos parámetros. De forma similar, un método asincrónico no puede devolver un valor por
referencia, aunque puede llamar a métodos con valores devueltos ref.
Para obtener más información y ejemplos, vea Tipos de valor devueltos asincrónicos (C#). Para más información
sobre cómo capturar excepciones en métodos asincrónicos, vea try-catch.
Las API asincrónicas en la programación de Windows Runtime tienen uno de los siguientes tipos de valor
devuelto, que son similares a las tareas:
IAsyncOperation<TResult>, lo que equivale a Task<TResult>
IAsyncAction, lo que equivale a Task
IAsyncActionWithProgress<TProgress>
IAsyncOperationWithProgress<TResult,TProgress>
Convención de nomenclatura
Por convención, los nombres de los métodos que devuelven tipos que suelen admitir "await" (por ejemplo,
Task , Task<T> , ValueTask y ValueTask<T> ) deben terminar por "Async". Los nombres de los métodos que
inician operaciones asincrónicas, pero que no devuelven un tipo que admite "await", no deben terminar en
"Async", pero pueden empezar por "Begin", "Start" o cualquier otro verbo para sugerir que este método no
devuelve ni genera el resultado de la operación.
Puede ignorar esta convención cuando un evento, clase base o contrato de interfaz sugieren un nombre
diferente. Por ejemplo, no se debería cambiar el nombre de los controladores de eventos, tales como
OnButtonClick .
Procedimiento para realizar varias Demuestra cómo comenzar varias Ejemplo de async: Make Multiple Web
solicitudes web en paralelo con async y tareas al mismo tiempo. Requests in Parallel (Ejemplo de async:
await (C#) Realizar varias solicitudes web en
paralelo)
Tipos de valor devueltos asincrónicos Muestra los tipos que los métodos
(C#) asincrónicos pueden devolver y explica
cuándo es apropiado cada uno de
ellos.
Vea también
async
await
Programación asincrónica
Información general de Async
Tipos de valor devueltos asincrónicos (C#)
16/09/2021 • 10 minutes to read
Los métodos asincrónicos pueden tener los siguientes tipos de valor devuelto:
Task, para un método asincrónico que realiza una operación pero no devuelve ningún valor.
Task<TResult>, para un método asincrónico que devuelve un valor.
void , para un controlador de eventos.
A partir de C# 7.0, cualquier tipo que tenga un método GetAwaiter accesible. El objeto devuelto por el
método GetAwaiter debe implementar la interfaz
System.Runtime.CompilerServices.ICriticalNotifyCompletion.
A partir C# 8.0, IAsyncEnumerable<T>, para un método asincrónico que devuelve una secuencia asincrónica.
Para obtener más información sobre los métodos asincrónicos, vea Programación asincrónica con async y await
(C#).
También existen varios tipos que son específicos de las cargas de trabajo de Windows:
DispatcherOperation: para las operaciones asincrónicas limitadas a Windows.
IAsyncAction, para las acciones asincrónicas en UWP que no devuelven un valor.
IAsyncActionWithProgress<TProgress>, para las acciones asincrónicas en UWP que notifican el progreso,
pero no devuelven un valor.
IAsyncOperation<TResult>: para las operaciones asincrónicas en UWP que devuelven un valor.
IAsyncOperationWithProgress<TResult,TProgress>: para las operaciones asincrónicas en UWP que informan
del progreso y devuelven un valor.
Console.WriteLine($"Today is {DateTime.Now:D}");
Console.WriteLine($"The current time is {DateTime.Now.TimeOfDay:t}");
Console.WriteLine("The current temperature is 76 degrees.");
}
Se espera a WaitAndApologizeAsync mediante una instrucción await en lugar de una expresión await, similar a la
instrucción de llamada para un método sincrónico que devuelve void. En este caso, la aplicación de un operador
await no genera un valor. Cuando el operando derecho de await es Task<TResult>, la expresión await genera
un resultado de T . Cuando el operando derecho de await es Task, await y su operando son una instrucción.
Puede separar la llamada a WaitAndApologizeAsync desde la aplicación de un operador await, como muestra el
código siguiente. Pero recuerde que una Task no tiene una propiedad Result y que no se genera ningún valor
cuando se aplica un operador await a una Task .
El código siguiente separa la llamada del método WaitAndApologizeAsync de la espera de la tarea que el método
devuelve.
string output =
$"Today is {DateTime.Now:D}\n" +
$"The current time is {DateTime.Now.TimeOfDay:t}\n" +
"The current temperature is 76 degrees.\n";
await waitAndApologizeTask;
Console.WriteLine(output);
Console.WriteLine(message);
}
int leisureHours =
today is DayOfWeek.Saturday || today is DayOfWeek.Sunday
? 16 : 5;
return leisureHours;
}
// Example output:
// Today is Wednesday, May 24, 2017
// Today's hours of leisure: 5
Cuando se llama a GetLeisureHoursAsync desde una expresión await en el método ShowTodaysInfo , esta
recupera el valor entero (el valor de leisureHours ) que está almacenado en la tarea que devuelve el método
GetLeisureHours . Para más información sobre las expresiones await, vea await.
Puede comprender mejor cómo await recupera el resultado de Task<T> si separa la llamada a
GetLeisureHoursAsync de la aplicación de await , como se muestra en el código siguiente. Una llamada al
método GetLeisureHoursAsync que no se espera inmediatamente devuelve Task<int> , como se podría esperar
de la declaración del método. La tarea se asigna a la variable getLeisureHoursTask en el ejemplo. Dado que
getLeisureHoursTask es Task<TResult>, contiene una propiedad Result de tipo TResult . En este caso, TResult
representa un tipo entero. Cuando await se aplica a getLeisureHoursTask , la expresión await se evalúa en el
contenido de la propiedad Result de getLeisureHoursTask . El valor se asigna a la variable ret .
IMPORTANT
La propiedad Result es una propiedad de bloqueo. Si se intenta acceder a ella antes de que termine su tarea, se bloquea el
subproceso que está activo actualmente hasta que finaliza la tarea y el valor está disponible. En la mayoría de los casos, se
debe tener acceso al valor usando await en lugar de tener acceso directamente a la propiedad.
En el ejemplo anterior se ha recuperado el valor de la propiedad Result para bloquear el subproceso principal de manera
que el método Main pueda imprimir el mensaje ( message ) en la consola antes de que finalice la aplicación.
string message =
$"Today is {DateTime.Today:D}\n" +
"Today's hours of leisure: " +
$"{await getLeisureHoursTask}";
Console.WriteLine(message);
using System;
using System.Threading.Tasks;
button.Clicked += OnButtonClicked1;
button.Clicked += OnButtonClicked2Async;
button.Clicked += OnButtonClicked3;
await secondHandlerFinished;
}
class Program
{
static readonly Random s_rnd = new Random();
La escritura de un tipo de valor devuelto asincrónico generalizado es un escenario avanzado y está destinado
para su uso en entornos especializados. En su lugar, considere la posibilidad de usar los tipos Task , Task<T> y
ValueTask<T> , que abarcan la mayoría de los escenarios del código asincrónico.
En C# 10.0 y versiones posteriores, puede aplicar el atributo AsyncMethodBuilder a un método asincrónico (en
lugar de la declaración de tipo de valor devuelto asincrónico) para invalidar el generador de ese tipo.
Normalmente, este atributo se aplica para usar un generador diferente proporcionado en el entorno de
ejecución de .NET.
En el ejemplo anterior, las líneas de una cadena se leen de forma asincrónica. Una vez que se ha leído cada línea,
el código enumera cada palabra de la cadena. Los autores de la llamada enumerarían cada palabra mediante la
instrucción await foreach . El método espera cuando necesita leer de forma asincrónica la línea siguiente de la
cadena de origen.
Vea también
FromResult
Procesamiento de tareas asincrónicas a medida que se completan
Programación asincrónica con async y await (C#)
async
await
Cancelación de una lista de tareas (C#)
16/09/2021 • 4 minutes to read
Puede cancelar una aplicación de consola asincrónica si no quiere esperar a que termine. Mediante el ejemplo
de este tema, puede agregar una cancelación a una aplicación que descargue el contenido de una lista de sitios
web. Puede cancelar muchas tareas asociando la instancia de CancellationTokenSource a cada tarea. Si se
presiona la tecla Entrar, se cancelan todas las tareas que aún no se han completado.
Esta tutorial abarca lo siguiente:
Creación de una aplicación de consola de .NET
Escritura de una aplicación asincrónica que admite la cancelación
Demostración de la señalización de una cancelación
Requisitos previos
Este tutorial requiere lo siguiente:
.NET 5.0 o un SDK posterior
Entorno de desarrollo integrado (IDE)
Se recomienda usar Visual Studio, Visual Studio Code o Visual Studio para Mac.
Creación de una aplicación de ejemplo
Cree una nueva aplicación de consola de .NET Core. Puede crear una mediante el comando dotnet new console
o desde Visual Studio. Abra el archivo Program.cs en su editor de código favorito.
Reemplazo de instrucciones using
Reemplace las instrucciones using existentes por estas declaraciones:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
Adición de campos
Dentro de la definición de la clase Program , agregue estos tres campos:
static readonly CancellationTokenSource s_cts = new CancellationTokenSource();
Console.WriteLine("Application ending.");
}
El método Main actualizado ahora se considera un método Async main, el cual permite un punto de entrada
asincrónico en el archivo ejecutable. Escribe algunos mensajes informativos en la consola y, luego, declara una
instancia de Task denominada cancelTask , la cual leerá las pulsaciones de teclas de la consola. Si se presiona la
tecla Entrar, se realiza una llamada a CancellationTokenSource.Cancel(). Esto indicará la cancelación. Después,
se asigna la variable sumPageSizesTask desde el método SumPageSizesAsync y ambas tareas se pasan a
Task.WhenAny(Task[]), que continuará cuando se complete cualquiera de las dos tareas.
int total = 0;
foreach (string url in s_urlList)
{
int contentLength = await ProcessUrlAsync(url, s_client, s_cts.Token);
total += contentLength;
}
stopwatch.Stop();
El método comienza creando una instancia e iniciando una clase Stopwatch. Luego, recorre en bucle cada
dirección URL en s_urlList y llama a ProcessUrlAsync . Con cada iteración, se pasa el token s_cts.Token al
método ProcessUrlAsync y el código devuelve una clase Task<TResult>, donde TResult es un entero:
int total = 0;
foreach (string url in s_urlList)
{
int contentLength = await ProcessUrlAsync(url, s_client, s_cts.Token);
total += contentLength;
}
return content.Length;
}
Para cualquier dirección URL, el método usará la instancia de client proporcionada para obtener la respuesta
como byte[] . La instancia de CancellationToken se pasa a los métodos HttpClient.GetAsync(String,
CancellationToken) y HttpContent.ReadAsByteArrayAsync(). El token ( token ) se usa para registrar la cancelación
solicitada. La longitud se devuelve después de que la dirección URL y la longitud se escriban en la consola.
Ejemplo de resultado de la aplicación
Application started.
Press the ENTER key to cancel...
https://docs.microsoft.com 37,357
https://docs.microsoft.com/aspnet/core 85,589
https://docs.microsoft.com/azure 398,939
https://docs.microsoft.com/azure/devops 73,663
https://docs.microsoft.com/dotnet 67,452
https://docs.microsoft.com/dynamics365 48,582
https://docs.microsoft.com/education 22,924
Application ending.
Ejemplo completo
El código siguiente es el texto completo del archivo Program.cs para el ejemplo.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static readonly CancellationTokenSource s_cts = new CancellationTokenSource();
Console.WriteLine("Application ending.");
}
int total = 0;
foreach (string url in s_urlList)
{
int contentLength = await ProcessUrlAsync(url, s_client, s_cts.Token);
total += contentLength;
}
stopwatch.Stop();
return content.Length;
}
}
Vea también
CancellationToken
CancellationTokenSource
Programación asincrónica con async y await (C#)
Pasos siguientes
Cancelar tareas asincrónicas tras un período de tiempo (C#)
Cancelar tareas asincrónicas tras un período de
tiempo (C#)
16/09/2021 • 2 minutes to read
Puede cancelar una operación asincrónica después de un período de tiempo con el método
CancellationTokenSource.CancelAfter si no quiere esperar a que finalice la operación. Este método programa la
cancelación de las tareas asociadas que no se completen en el período de tiempo designado por la expresión
CancelAfter .
Este ejemplo se agrega al código que se desarrolla en el tutorial Cancelación de una lista de tareas (C#) para
descargar una lista de sitios web y mostrar la longitud del contenido de cada uno de ellos.
Esta tutorial abarca lo siguiente:
Actualización de una aplicación de consola .NET existente
Programación de una cancelación
Requisitos previos
Este tutorial requiere lo siguiente:
Que haya creado una aplicación en el tutorial Cancelación de una lista de tareas (C#)
.NET 5.0 o un SDK posterior
Entorno de desarrollo integrado (IDE)
Se recomienda usar Visual Studio, Visual Studio Code o Visual Studio para Mac.
try
{
s_cts.CancelAfter(3500);
await SumPageSizesAsync();
}
catch (TaskCanceledException)
{
Console.WriteLine("\nTasks cancelled: timed out.\n");
}
finally
{
s_cts.Dispose();
}
Console.WriteLine("Application ending.");
}
El método actualizado Main escribe algunos mensajes informativos en la consola. En la instrucción try catch,
una llamada a CancellationTokenSource.CancelAfter(Int32) programa una cancelación. Esto indicará la
cancelación pasado un período de tiempo.
Después, se espera al método SumPageSizesAsync . Si el procesamiento de todas las direcciones URL se produce
más rápido que la cancelación programada, la aplicación finaliza. Pero si la cancelación programada se
desencadena antes de que se procesen todas las direcciones URL, se produce una excepción
TaskCanceledException.
Ejemplo de resultado de la aplicación
Application started.
https://docs.microsoft.com 37,357
https://docs.microsoft.com/aspnet/core 85,589
https://docs.microsoft.com/azure 398,939
https://docs.microsoft.com/azure/devops 73,663
Application ending.
Ejemplo completo
El código siguiente es el texto completo del archivo Program.cs para el ejemplo.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static readonly CancellationTokenSource s_cts = new CancellationTokenSource();
try
{
s_cts.CancelAfter(3500);
await SumPageSizesAsync();
}
catch (TaskCanceledException)
{
Console.WriteLine("\nTasks cancelled: timed out.\n");
}
finally
{
s_cts.Dispose();
}
Console.WriteLine("Application ending.");
}
int total = 0;
foreach (string url in s_urlList)
{
int contentLength = await ProcessUrlAsync(url, s_client, s_cts.Token);
total += contentLength;
}
stopwatch.Stop();
return content.Length;
}
}
Vea también
CancellationToken
CancellationTokenSource
Programación asincrónica con async y await (C#)
Cancelación de una lista de tareas (C#)
Iniciar varias tareas asincrónicas y procesarlas a
medida que se completan (C#)
16/09/2021 • 4 minutes to read
Si usa Task.WhenAny, puede iniciar varias tareas a la vez y procesarlas una por una a medida que se completen,
en lugar de procesarlas en el orden en el que se han iniciado.
En el siguiente ejemplo se usa una consulta para crear una colección de tareas. Cada tarea descarga el contenido
de un sitio web especificado. En cada iteración de un bucle while, una llamada awaited a WhenAny devuelve la
tarea en la colección de tareas que termine primero su descarga. Esa tarea se quita de la colección y se procesa.
El bucle se repite hasta que la colección no contiene más tareas.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
Adición de campos
Dentro de la definición de la clase Program , agregue los dos campos siguientes:
static readonly HttpClient s_client = new HttpClient
{
MaxResponseContentBufferSize = 1_000_000
};
HttpClient expone la capacidad de enviar solicitudes HTTP y de recibir respuestas HTTP. s_urlList contiene
todas las direcciones URL que planea procesar la aplicación.
El método Main actualizado ahora se considera un método Async main, el cual permite un punto de entrada
asincrónico en el archivo ejecutable. Se trata de una llamada a SumPageSizesAsync .
IEnumerable<Task<int>> downloadTasksQuery =
from url in s_urlList
select ProcessUrlAsync(url, s_client);
int total = 0;
while (downloadTasks.Any())
{
Task<int> finishedTask = await Task.WhenAny(downloadTasks);
downloadTasks.Remove(finishedTask);
total += await finishedTask;
}
stopwatch.Stop();
El método comienza creando una instancia e iniciando una clase Stopwatch. Después, incluye una consulta que,
cuando se ejecuta, crea una colección de tareas. Cada llamada a ProcessUrlAsync en el siguiente código
devuelve un objeto Task<TResult>, donde TResult es un entero:
IEnumerable<Task<int>> downloadTasksQuery =
from url in s_urlList
select ProcessUrlAsync(url, s_client);
Debido a la ejecución diferida con LINQ, se llama a Enumerable.ToList para iniciar cada tarea.
El bucle while realiza los pasos siguientes para cada tarea de la colección:
1. Espera una llamada a WhenAny para identificar la primera tarea de la colección que ha finalizado su
descarga.
downloadTasks.Remove(finishedTask);
return content.Length;
}
Para cualquier dirección URL, el método usará la instancia de client proporcionada para obtener la respuesta
como byte[] . La longitud se devuelve después de que la dirección URL y la longitud se escriban en la consola.
Ejecute el programa varias veces para comprobar que las longitudes que se han descargado no aparecen
siempre en el mismo orden.
Cau t i on
Puede usar WhenAny en un bucle, como se describe en el ejemplo, para solucionar problemas que implican un
número reducido de tareas. Sin embargo, otros enfoques son más eficaces si hay que procesar un gran número
de tareas. Para más información y ejemplos, vea Processing Tasks as they complete (Procesar tareas a medida
que se completan).
Ejemplo completo
El código siguiente es el texto completo del archivo Program.cs para el ejemplo.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
namespace ProcessTasksAsTheyFinish
{
class Program
{
static readonly HttpClient s_client = new HttpClient
{
MaxResponseContentBufferSize = 1_000_000
};
IEnumerable<Task<int>> downloadTasksQuery =
from url in s_urlList
select ProcessUrlAsync(url, s_client);
int total = 0;
while (downloadTasks.Any())
{
Task<int> finishedTask = await Task.WhenAny(downloadTasks);
downloadTasks.Remove(finishedTask);
total += await finishedTask;
}
stopwatch.Stop();
return content.Length;
}
}
}
// Example output:
// https://docs.microsoft.com/windows 25,513
// https://docs.microsoft.com/gaming 30,705
// https://docs.microsoft.com/dotnet 69,626
// https://docs.microsoft.com/dynamics365 50,756
// https://docs.microsoft.com/surface 35,519
// https://docs.microsoft.com 39,531
// https://docs.microsoft.com/azure/devops 75,837
// https://docs.microsoft.com/xamarin 60,284
// https://docs.microsoft.com/system-center 43,444
// https://docs.microsoft.com/enterprise-mobility-security 28,946
// https://docs.microsoft.com/microsoft-365 43,278
// https://docs.microsoft.com/visualstudio 31,414
// https://docs.microsoft.com/office 42,292
// https://docs.microsoft.com/azure 401,113
// https://docs.microsoft.com/graph 46,831
// https://docs.microsoft.com/education 25,098
// https://docs.microsoft.com/powershell 58,173
// https://docs.microsoft.com/aspnet/core 87,763
// https://docs.microsoft.com/sql 53,362
Puede usar la característica async para acceder a archivos. Con la característica async, se puede llamar a
métodos asincrónicos sin usar devoluciones de llamada ni dividir el código en varios métodos o expresiones
lambda. Para convertir código sincrónico en asincrónico, basta con llamar a un método asincrónico y no a un
método sincrónico y agregar algunas palabras clave al código.
Podrían considerarse los siguientes motivos para agregar asincronía a las llamadas de acceso a archivos:
La asincronía hace que las aplicaciones de interfaz de usuario tengan mayor capacidad de respuesta porque
el subproceso de interfaz de usuario que inicia la operación puede realizar otro trabajo. Si el subproceso de
interfaz de usuario debe ejecutar código que tarda mucho tiempo (por ejemplo, más de 50 milisegundos),
puede inmovilizar la interfaz de usuario hasta que la E/S se complete y el subproceso de interfaz de usuario
pueda volver a procesar la entrada de teclado y de mouse y otros eventos.
La asincronía mejora la escalabilidad de ASP.NET y otras aplicaciones basadas en servidor reduciendo la
necesidad de subproceso. Si la aplicación usa un subproceso dedicado por respuesta y se procesa un millar
de solicitudes simultáneamente, se necesitan mil subprocesos. Las operaciones asincrónicas no suelen
necesitar un subproceso durante la espera. Usan el subproceso existente de finalización de E/S brevemente al
final.
Puede que la latencia de una operación de acceso a archivos sea muy baja en las condiciones actuales, pero
puede aumentar mucho en el futuro. Por ejemplo, se puede mover un archivo a un servidor que está a escala
mundial.
La sobrecarga resultante de usar la característica Async es pequeña.
Las tareas asincrónicas se pueden ejecutar fácilmente en paralelo.
Escritura de texto
En los siguientes ejemplos se escribe texto en un archivo. En cada instrucción await, el método finaliza
inmediatamente. Cuando se complete la E/S de archivo, el método se reanuda en la instrucción que sigue a la
instrucción await. El modificador async se encuentra en la definición de métodos que usan la instrucción await.
Ejemplo sencillo
public async Task SimpleWriteAsync()
{
string filePath = "simple.txt";
string text = $"Hello World";
La primera instrucción devuelve una tarea e inicia el procesamiento de archivos. La segunda instrucción con
await finaliza el método inmediatamente y devuelve otra tarea. Después, cuando se complete el procesamiento
de archivos, la ejecución vuelve a la instrucción que sigue a la instrucción await.
Lectura de texto
En los ejemplos siguientes se lee texto de un archivo.
Ejemplo sencillo
Console.WriteLine(text);
}
return sb.ToString();
}
writeTaskList.Add(File.WriteAllTextAsync(filePath, text));
}
await Task.WhenAll(writeTaskList);
}
try
{
string folder = Directory.CreateDirectory("tempfolder").Name;
IList<Task> writeTaskList = new List<Task>();
var sourceStream =
new FileStream(
filePath,
FileMode.Create, FileAccess.Write, FileShare.None,
bufferSize: 4096, useAsync: true);
writeTaskList.Add(writeTask);
}
await Task.WhenAll(writeTaskList);
}
finally
{
foreach (FileStream sourceStream in sourceStreams)
{
sourceStream.Close();
}
}
}
Al usar los métodos WriteAsync y ReadAsync, puede especificar un CancellationToken, que puede usar para
cancelar la operación en mitad de la secuencia. Para más información, consulte Cancelación de subprocesos
administrados.
Vea también
Programación asincrónica con async y await (C#)
Tipos de valor devueltos asincrónicos (C#)
Atributos (C#)
16/09/2021 • 5 minutes to read
Los atributos proporcionan un método eficaz para asociar metadatos, o información declarativa, con código
(ensamblados, tipos, métodos, propiedades, etc.). Después de asociar un atributo con una entidad de programa,
se puede consultar el atributo en tiempo de ejecución mediante la utilización de una técnica denominada
reflexión. Para obtener más información, vea Reflexión (C#).
Los atributos tienen las propiedades siguientes:
Los atributos agregan metadatos al programa. Los metadatos son información sobre los tipos definidos en
un programa. Todos los ensamblados .NET contienen un conjunto de metadatos específico que describe los
tipos y miembros de tipo definidos en el ensamblado. Puede agregar atributos personalizados para
especificar cualquier información adicional que sea necesaria. Para obtener más información, vea Crear
atributos personalizados (C#).
Puede aplicar uno o más atributos a todos los ensamblados, módulos o elementos de programa más
pequeños como clases y propiedades.
Los atributos pueden aceptar argumentos de la misma manera que los métodos y las propiedades.
El programa puede examinar sus propios metadatos o los metadatos de otros programas mediante la
reflexión. Para obtener más información, consulte Acceder a atributos mediante reflexión (C#).
Uso de atributos
Los atributos se pueden colocar en la mayoría de las declaraciones, aunque un determinado atributo podría
restringir los tipos de declaraciones en que es válido. En C#, para especificar un atributo se coloca su nombre
entre corchetes ([]) por encima de la declaración de la entidad a la que se aplica.
En este ejemplo, el atributo SerializableAttribute se usa para aplicar una característica específica a una clase:
[Serializable]
public class SampleClass
{
// Objects of this type can be serialized.
}
[System.Runtime.InteropServices.DllImport("user32.dll")]
extern static void SampleMethod();
En una declaración se puede colocar más de un atributo, como se muestra en el siguiente ejemplo:
using System.Runtime.InteropServices;
Algunos atributos se pueden especificar más de una vez para una entidad determinada. Un ejemplo de este tipo
de atributos multiuso es ConditionalAttribute:
[Conditional("DEBUG"), Conditional("TEST1")]
void TraceMethod()
{
// ...
}
NOTE
Por convención, todos los nombres de atributos terminan con la palabra "Attribute" para distinguirlos de otros elementos
de las bibliotecas de .NET. Sin embargo, no es necesario especificar el sufijo de atributo cuando utiliza atributos en el
código. Por ejemplo, [DllImport] es equivalente a [DllImportAttribute] , pero DllImportAttribute es el nombre
real del atributo en la biblioteca de clases .NET.
Parámetros de atributo
Muchos atributos tienen parámetros, que pueden ser posicionales, sin nombre o con nombre. Los parámetros
posicionales deben especificarse en un orden determinado y no se pueden omitir. Los parámetros con nombre
son opcionales y pueden especificarse en cualquier orden. Los parámetros posicionales se especifican en primer
lugar. Por ejemplo, estos tres atributos son equivalentes:
[DllImport("user32.dll")]
[DllImport("user32.dll", SetLastError=false, ExactSpelling=false)]
[DllImport("user32.dll", ExactSpelling=false, SetLastError=false)]
El primer parámetro, el nombre del archivo DLL, es posicional y siempre va primero; los demás tienen un
nombre. En este caso, ambos parámetros con nombre tienen el estado false de forma predeterminada, por lo
que se pueden omitir. Los parámetros posicionales corresponden a los parámetros del constructor de atributos.
Los parámetros con nombre u opcionales corresponden a propiedades o campos del atributo. Consulte la
documentación del atributo individual para obtener información sobre los valores de parámetro
predeterminados.
Para obtener más información sobre los tipos de parámetro admitidos, vea la sección Atributos de la
Especificación del lenguaje C#
Destinos de atributo
El destino de un atributo es la entidad a la que se aplica dicho atributo. Por ejemplo, puede aplicar un atributo a
una clase, un método determinado o un ensamblado completo. De forma predeterminada, el atributo se aplica
al elemento que sigue. Pero puede identificar explícitamente, por ejemplo, si se aplica un atributo a un método, a
su parámetro o a su valor devuelto.
Para identificar un destino de atributo de forma explícita, use la sintaxis siguiente:
[target : attribute-list]
event evento
property Propiedad.
Debe especificar el valor de destino field para aplicar un atributo al campo de respaldo creado para una
propiedad implementada automáticamente.
En el ejemplo siguiente se muestra cómo aplicar atributos a ensamblados y módulos. Para obtener más
información, vea Atributos comunes (C#).
using System;
using System.Reflection;
[assembly: AssemblyTitleAttribute("Production assembly 4")]
[module: CLSCompliant(true)]
En el ejemplo siguiente, se muestra cómo aplicar atributos a métodos, parámetros de método y valores
devueltos por métodos en C#.
// applies to method
[method: ValidatedContract]
int Method2() { return 0; }
// applies to parameter
int Method3([ValidatedContract] string contract) { return 0; }
NOTE
Independientemente de los destinos en los que ValidatedContract se define para que sea válido, debe especificarse el
destino return , incluso si ValidatedContract se ha definido para que se aplique solo a los valores devueltos. En otras
palabras, el compilador no usará información de AttributeUsage para resolver destinos de atributo ambiguos. Para
obtener más información, consulte AttributeUsage (C#).
Usos comunes de los atributos
La lista siguiente incluye algunos de los usos comunes de atributos en el código:
Marcar métodos con el atributo WebMethod en los servicios web para indicar que el método debe ser
invocable a través del protocolo SOAP. Para obtener más información, vea WebMethodAttribute.
Describir cómo serializar parámetros de método al interoperar con código nativo. Para obtener más
información, vea MarshalAsAttribute.
Describir las propiedades COM para clases, métodos e interfaces.
Llamar al código no administrado mediante la clase DllImportAttribute.
Describir los ensamblados en cuanto a título, versión, descripción o marca.
Describir qué miembros de una clase serializar para la persistencia.
Describir cómo realizar asignaciones entre los miembros de clase y los nodos XML para la serialización XML.
Describir los requisitos de seguridad para los métodos.
Especificar las características utilizadas para reforzar la seguridad.
Controlar optimizaciones mediante el compilador Just-In-Time (JIT) para que el código siga siendo fácil de
depurar.
Obtener información sobre el llamador de un método.
Secciones relacionadas
Para obtener más información, consulte:
Crear atributos personalizados (C#)
Acceder a atributos mediante reflexión (C#)
Procedimiento para: Crear una unión de C/C++ mediante atributos (C#)
Atributos comunes (C#)
Información del llamador (C#)
Vea también
Guía de programación de C#
Reflexión (C#)
Atributos
Uso de atributos en C#
Crear atributos personalizados (C#)
16/09/2021 • 2 minutes to read
Para crear sus propios atributos personalizados, defina una clase de atributo derivada directa o indirectamente
de Attribute, que agiliza y facilita la identificación de las definiciones de atributos en los metadatos. Imagínese
que desea etiquetar tipos con el nombre del programador que los escribió. Puede definir una clase de atributos
Author personalizada:
[System.AttributeUsage(System.AttributeTargets.Class |
System.AttributeTargets.Struct)
]
public class AuthorAttribute : System.Attribute
{
private string name;
public double version;
El nombre de la clase AuthorAttribute es el nombre del atributo, Author , al que se le agrega el sufijo
Attribute . Se deriva de System.Attribute , por lo que es una clase de atributo personalizada. Los parámetros
del constructor son los parámetros posicionales del atributo personalizado. En este ejemplo, name es un
parámetro posicional. Las propiedades o los campos públicos de lectura y escritura son parámetros con
nombre. En este caso, version es el único parámetro con nombre. Observe el uso del atributo AttributeUsage
para hacer que el atributo Author sea válido solo en las declaraciones de clase y de struct .
Puede usar este nuevo atributo de la siguiente manera:
AttributeUsage tiene un parámetro con nombre, AllowMultiple , con el que puede hacer que un atributo
personalizado sea multiuso o de un solo uso. En el ejemplo de código siguiente se crea un atributo multiuso.
[System.AttributeUsage(System.AttributeTargets.Class |
System.AttributeTargets.Struct,
AllowMultiple = true) // multiuse attribute
]
public class AuthorAttribute : System.Attribute
En el ejemplo de código siguiente se aplican varios atributos del mismo tipo a una clase.
[Author("P. Ackerman", version = 1.1)]
[Author("R. Koch", version = 1.2)]
class SampleClass
{
// P. Ackerman's code goes here...
// R. Koch's code goes here...
}
Vea también
System.Reflection
Guía de programación de C#
Escribir atributos personalizados
Reflexión (C#)
Atributos (C#)
Acceder a atributos mediante reflexión (C#)
AttributeUsage (C#)
Acceder a atributos mediante reflexión (C#)
16/09/2021 • 2 minutes to read
El hecho de que pueda definir atributos personalizados y colocarlos en el código fuente no serviría de mucho si
no existiera ninguna forma de recuperar la información y actuar en consecuencia. Mediante la reflexión, puede
recuperar la información que se ha definido con atributos personalizados. El método clave es
GetCustomAttributes , que devuelve una matriz de objetos que son los equivalentes en tiempo de ejecución de
los atributos de código fuente. Este método tiene varias versiones sobrecargadas. Para obtener más
información, vea Attribute.
Una especificación de atributo como:
En cambio, el código no se ejecuta hasta que se consulta a SampleClass sobre los atributos. Llamar a
GetCustomAttributes en SampleClass hace que se cree e inicialice un objeto Author como se ha mostrado
anteriormente. Si la clase tiene otros atributos, se crean otros objetos de atributo de forma similar. Luego,
GetCustomAttributes devuelve el objeto Author y cualquier otro objeto de atributo en una matriz. Después,
puede recorrer en iteración esta matriz, determinar qué atributos se han aplicado según el tipo de cada
elemento de la matriz y extraer información de los objetos de atributo.
Ejemplo
Este es un ejemplo completo. Se define un atributo personalizado, se aplica a varias entidades y se recupera
mediante reflexión.
// Multiuse attribute.
[System.AttributeUsage(System.AttributeTargets.Class |
System.AttributeTargets.Struct,
AllowMultiple = true) // Multiuse attribute.
]
public class Author : System.Attribute
{
string name;
public double version;
// Default value.
version = 1.0;
}
class TestAuthorAttribute
{
static void Test()
{
PrintAuthorInfo(typeof(FirstClass));
PrintAuthorInfo(typeof(SecondClass));
PrintAuthorInfo(typeof(ThirdClass));
}
// Using reflection.
System.Attribute[] attrs = System.Attribute.GetCustomAttributes(t); // Reflection.
// Displaying output.
foreach (System.Attribute attr in attrs)
{
if (attr is Author)
{
Author a = (Author)attr;
System.Console.WriteLine(" {0}, version {1:f}", a.GetName(), a.version);
}
}
}
}
/* Output:
Author information for FirstClass
P. Ackerman, version 1.00
Author information for SecondClass
Author information for ThirdClass
R. Koch, version 2.00
P. Ackerman, version 1.00
*/
Vea también
System.Reflection
Attribute
Guía de programación de C#
Recuperar información almacenada en atributos
Reflexión (C#)
Atributos (C#)
Crear atributos personalizados (C#)
Procedimiento para crear una unión de C o C++
mediante atributos (C#)
16/09/2021 • 2 minutes to read
Mediante el uso de atributos, puede personalizar la manera en que los structs se disponen en la memoria. Por
ejemplo, puede crear lo que se conoce como una unión en C/ C++ mediante los atributos
StructLayout(LayoutKind.Explicit) y FieldOffset .
Ejemplos
En este segmento de código, todos los campos de TestUnion empiezan en la misma ubicación en la memoria.
[System.Runtime.InteropServices.StructLayout(LayoutKind.Explicit)]
struct TestUnion
{
[System.Runtime.InteropServices.FieldOffset(0)]
public int i;
[System.Runtime.InteropServices.FieldOffset(0)]
public double d;
[System.Runtime.InteropServices.FieldOffset(0)]
public char c;
[System.Runtime.InteropServices.FieldOffset(0)]
public byte b;
}
A continuación se muestra otro ejemplo en el que los campos empiezan en ubicaciones diferentes establecidas
explícitamente.
// Add a using directive for System.Runtime.InteropServices.
[System.Runtime.InteropServices.StructLayout(LayoutKind.Explicit)]
struct TestExplicit
{
[System.Runtime.InteropServices.FieldOffset(0)]
public long lg;
[System.Runtime.InteropServices.FieldOffset(0)]
public int i1;
[System.Runtime.InteropServices.FieldOffset(0)]
public int i2;
[System.Runtime.InteropServices.FieldOffset(8)]
public double d;
[System.Runtime.InteropServices.FieldOffset(12)]
public char c;
[System.Runtime.InteropServices.FieldOffset(14)]
public byte b;
}
Los dos campos enteros, i1 e i2 , tiene las mismas ubicaciones en la memoria que lg . Este tipo de control
sobre el diseño del struct es útil cuando se usa la invocación de plataforma.
Vea también
System.Reflection
Attribute
Guía de programación de C#
Atributos
Reflexión (C#)
Atributos (C#)
Crear atributos personalizados (C#)
Acceder a atributos mediante reflexión (C#)
Colecciones (C#)
16/09/2021 • 14 minutes to read
Para muchas aplicaciones, puede que desee crear y administrar grupos de objetos relacionados. Existen dos
formas de agrupar objetos: mediante la creación de matrices de objetos y con la creación de colecciones de
objetos.
Las matrices son muy útiles para crear y trabajar con un número fijo de objetos fuertemente tipados. Para
obtener información sobre las matrices, vea Matrices.
Las colecciones proporcionan una manera más flexible de trabajar con grupos de objetos. A diferencia de las
matrices, el grupo de objetos con el que trabaja puede aumentar y reducirse de manera dinámica a medida que
cambian las necesidades de la aplicación. Para algunas colecciones, puede asignar una clave a cualquier objeto
que incluya en la colección para, de este modo, recuperar rápidamente el objeto con la clave.
Una colección es una clase, por lo que debe declarar una instancia de la clase para poder agregar elementos a
dicha colección.
Si la colección contiene elementos de un solo tipo de datos, puede usar una de las clases del espacio de
nombres System.Collections.Generic. Una colección genérica cumple la seguridad de tipos para que ningún otro
tipo de datos se pueda agregar a ella. Cuando recupera un elemento de una colección genérica, no tiene que
determinar su tipo de datos ni convertirlo.
NOTE
Para los ejemplos de este tema, incluya las directivas using para los espacios de nombres System.Collections.Generic
y System.Linq .
En este tema
Uso de una colección Simple
Tipos de colecciones
Clases System.Collections.Generic
Clases System.Collections.Concurrent
Clases System.Collections
Implementación de una colección de pares de clave/valor
Uso de LINQ para tener acceso a una colección
Ordenar una colección
Definición de una colección personalizada
Iteradores
Si el contenido de una colección se conoce de antemano, puede usar un inicializador de colección para inicializar
la colección. Para obtener más información, vea Inicializadores de objeto y colección.
El ejemplo siguiente es el mismo que el ejemplo anterior, excepto que se usa un inicializador de colección para
agregar elementos a la colección.
Puede usar una instrucción for en lugar de una instrucción foreach para recorrer en iteración una colección.
Esto se consigue con el acceso a los elementos de la colección mediante la posición de índice. El índice de los
elementos comienza en 0 y termina en el número de elementos menos 1.
El ejemplo siguiente recorre en iteración los elementos de una colección mediante for en lugar de foreach .
El ejemplo siguiente quita elementos de una lista genérica. En lugar de una instrucción foreach , se usa una
instrucción for que procesa una iteración en orden descendente. Esto es porque el método RemoveAt hace
que los elementos después de un elemento quitado tengan un valor de índice inferior.
Para el tipo de elementos de List<T>, también puede definir su propia clase. En el ejemplo siguiente, la clase
Galaxy que usa List<T> se define en el código.
private static void IterateThroughList()
{
var theGalaxies = new List<Galaxy>
{
new Galaxy() { Name="Tadpole", MegaLightYears=400},
new Galaxy() { Name="Pinwheel", MegaLightYears=25},
new Galaxy() { Name="Milky Way", MegaLightYears=0},
new Galaxy() { Name="Andromeda", MegaLightYears=3}
};
// Output:
// Tadpole 400
// Pinwheel 25
// Milky Way 0
// Andromeda 3
}
Tipos de colecciones
.NET proporciona muchas colecciones comunes. Cada tipo de colección está diseñado para un fin específico.
En esta sección se describen algunas de las clases de colecciones comunes:
Clases System.Collections.Generic
Clases System.Collections.Concurrent
Clases System.Collections
Clases System.Collections.Generic
Puede crear una colección genérica mediante una de las clases del espacio de nombres
System.Collections.Generic. Una colección genérica es útil cuando todos los elementos de la colección tienen el
mismo tipo. Una colección genérica exige el establecimiento de fuertes tipos al permitir agregar solo los tipos
de datos deseados.
En la tabla siguiente se enumeran algunas de las clases usadas con frecuencia del espacio de nombres
System.Collections.Generic:
C L A SE DESC RIP C IÓ N
List<T> Representa una lista de objetos a los que puede tener acceso
el índice. Proporciona métodos para buscar, ordenar y
modificar listas.
C L A SE DESC RIP C IÓ N
Para obtener más información, vea Tipos de colección utilizados normalmente, Seleccionar una clase de
colección y System.Collections.Generic.
Clases System.Collections.Concurrent
En .NET Framework 4 y versiones posteriores, las colecciones del espacio de nombres
System.Collections.Concurrent proporcionan operaciones eficaces y seguras para subprocesos con el fin de
acceder a los elementos de la colección desde varios subprocesos.
Las clases del espacio de nombres System.Collections.Concurrent deben usarse en lugar de los tipos
correspondientes de los espacios de nombres System.Collections.Generic y System.Collections cada vez que
varios subprocesos tengan acceso de manera simultánea a la colección. Para obtener más información, vea
Colecciones seguras para subprocesos y System.Collections.Concurrent.
Algunas clases incluidas en el espacio de nombres System.Collections.Concurrent son BlockingCollection<T>,
ConcurrentDictionary<TKey,TValue>, ConcurrentQueue<T> y ConcurrentStack<T>.
Clases System.Collections
Las clases del espacio de nombres System.Collections no almacenan los elementos como objetos de tipo
específico, sino como objetos del tipo Object .
Siempre que sea posible, debe usar las colecciones genéricas del espacio de nombres
System.Collections.Generic o del espacio de nombres System.Collections.Concurrent en lugar de los tipos
heredados del espacio de nombres System.Collections .
En la siguiente tabla se enumeran algunas de las clases usadas con frecuencia en el espacio de nombres
System.Collections :
C L A SE DESC RIP C IÓ N
return elements;
}
theElement.Symbol = symbol;
theElement.Name = name;
theElement.AtomicNumber = atomicNumber;
Para usar un inicializador de colección para compilar la colección Dictionary , puede reemplazar los métodos
BuildDictionary y AddToDictionary por el método siguiente.
private static Dictionary<string, Element> BuildDictionary2()
{
return new Dictionary<string, Element>
{
{"K",
new Element() { Symbol="K", Name="Potassium", AtomicNumber=19}},
{"Ca",
new Element() { Symbol="Ca", Name="Calcium", AtomicNumber=20}},
{"Sc",
new Element() { Symbol="Sc", Name="Scandium", AtomicNumber=21}},
{"Ti",
new Element() { Symbol="Ti", Name="Titanium", AtomicNumber=22}}
};
}
En el ejemplo siguiente se usa el método ContainsKey y la propiedad Item[] de Dictionary para encontrar
rápidamente un elemento por clave. La propiedad Item le permite tener acceso a un elemento de la colección
elements usando elements[symbol] en C#.
if (elements.ContainsKey(symbol) == false)
{
Console.WriteLine(symbol + " not found");
}
else
{
Element theElement = elements[symbol];
Console.WriteLine("found: " + theElement.Name);
}
}
En el ejemplo siguiente se usa en su lugar el método TryGetValue para encontrar rápidamente un elemento por
clave.
// LINQ Query.
var subset = from theElement in elements
where theElement.AtomicNumber < 22
orderby theElement.Name
select theElement;
// Output:
// Calcium 20
// Potassium 19
// Scandium 21
}
// Output:
// blue 50 car4
// blue 30 car5
// blue 20 car1
// green 50 car7
// green 10 car3
// red 60 car6
// red 50 car2
}
return compare;
}
}
// Collection class.
public class AllColors : System.Collections.IEnumerable
{
Color[] _colors =
{
new Color() { Name = "red" },
new Color() { Name = "blue" },
new Color() { Name = "green" }
};
// Custom enumerator.
private class ColorEnumerator : System.Collections.IEnumerator
{
private Color[] _colors;
private int _position = -1;
object System.Collections.IEnumerator.Current
{
get
{
return _colors[_position];
}
}
bool System.Collections.IEnumerator.MoveNext()
{
_position++;
return (_position < _colors.Length);
}
void System.Collections.IEnumerator.Reset()
{
{
_position = -1;
}
}
}
// Element class.
public class Color
{
public string Name { get; set; }
}
Iterators
Los iteradores se usan para efectuar una iteración personalizada en una colección. Un iterador puede ser un
método o un descriptor de acceso get . Un iterador usa una instrucción yield return para devolver cada
elemento de la colección a la vez.
Llame a un iterador mediante una instrucción foreach. Cada iteración del bucle foreach llama al iterador.
Cuando se alcanza una instrucción yield return en el iterador, se devuelve una expresión y se conserva la
ubicación actual en el código. La ejecución se reinicia desde esa ubicación la próxima vez que se llama al
iterador.
Para obtener más información, vea Iteradores (C#).
El siguiente ejemplo usa el método del iterador. El método del iterador tiene una instrucción yield return que
se encuentra dentro de un bucle for . En el método ListEvenNumbers , cada iteración del cuerpo de la instrucción
foreach crea una llamada al método iterador, que continúa con la siguiente instrucción yield return .
Vea también
Inicializadores de objeto y colección
Conceptos de programación (C#)
Option Strict (instrucción)
LINQ to Objects (C#)
Parallel LINQ (PLINQ)
Colecciones y estructuras de datos
Seleccionar una clase de colección
Comparaciones y ordenaciones en colecciones
Cuándo utilizar colecciones genéricas
Instrucciones de iteración
Covarianza y contravarianza (C#)
16/09/2021 • 3 minutes to read
En C#, la covarianza y la contravarianza habilitan la conversión de referencias implícita de tipos de matriz, tipos
de delegado y argumentos de tipo genérico. La covarianza conserva la compatibilidad de asignaciones y la
contravarianza la invierte.
El siguiente código muestra la diferencia entre la compatibilidad de asignaciones, la covarianza y la
contravarianza.
// Assignment compatibility.
string str = "test";
// An object of a more derived type is assigned to an object of a less derived type.
object obj = str;
// Covariance.
IEnumerable<string> strings = new List<string>();
// An object that is instantiated with a more derived type argument
// is assigned to an object instantiated with a less derived type argument.
// Assignment compatibility is preserved.
IEnumerable<object> objects = strings;
// Contravariance.
// Assume that the following method is in the class:
// static void SetObject(object o) { }
Action<object> actObject = SetObject;
// An object that is instantiated with a less derived type argument
// is assigned to an object instantiated with a more derived type argument.
// Assignment compatibility is reversed.
Action<string> actString = actObject;
La covarianza de matrices permite la conversión implícita de una matriz de un tipo más derivado a una matriz
de un tipo menos derivado. Pero esta operación no es segura, tal como se muestra en el ejemplo de código
siguiente.
La compatibilidad de la covarianza y la contravarianza con grupos de métodos permite hacer coincidir firmas de
método con tipos de delegado. Esto le permite asignar a los delegados no solo métodos con firmas
coincidentes, sino métodos que devuelven tipos más derivados (covarianza) o que aceptan parámetros con tipos
menos derivados (contravarianza) que el especificado por el tipo de delegado. Para obtener más información,
vea Varianza en delegados (C#) y Usar varianza en delegados (C#).
En el ejemplo de código siguiente, se muestra la compatibilidad de covarianza y contravarianza con grupos de
métodos.
static object GetObject() { return null; }
static void SetObject(object obj) { }
Un delegado o interfaz genéricos se denominan variante si sus parámetros genéricos se declaran como
covariantes o contravariantes. C# le permite crear sus propias interfaces y delegados variantes. Para obtener
más información, consulte Crear interfaces genéricas variantes (C#) y Varianza en delegados (C#).
Temas relacionados
T IT L E DESC RIP C IÓ N
Crear interfaces genéricas variantes (C#) Se muestra cómo crear interfaces variantes personalizadas.
Usar la varianza en interfaces para las colecciones genéricas Se muestra cómo la compatibilidad de covarianza y
(C#) contravarianza en las interfaces IEnumerable<T> y
IComparable<T> puede ayudarle a volver a usar el código.
Usar varianza para los delegados genéricos Func y Action Se muestra cómo la compatibilidad de covarianza y
(C#) contravarianza en los delegados Func y Action puede
ayudarle a volver a usar el código.
Varianza en interfaces genéricas (C#)
16/09/2021 • 2 minutes to read
En .NET Framework 4 se ha presentado la compatibilidad con la varianza para varias interfaces genéricas
existentes. La compatibilidad con la varianza permite la conversión implícita de clases que implementan estas
interfaces.
A partir de .NET Framework 4, las siguientes interfaces son variantes:
IEnumerable<T> (T es covariante)
IEnumerator<T> (T es covariante)
IQueryable<T> (T es covariante)
IGrouping<TKey,TElement> ( TKey y TElement son covariantes)
IComparer<T> (T es contravariante)
IEqualityComparer<T> (T es contravariante)
IComparable<T> (T es contravariante)
A partir de .NET Framework 4.5, las siguientes interfaces son variantes:
IReadOnlyList<T> (T es covariante)
IReadOnlyCollection<T> (T es covariante)
La covarianza permite que un método tenga un tipo de valor devuelto más derivado que los que se definen en
los parámetros de tipo genérico de la interfaz. Para ilustrar la característica de la covarianza, considere estas
interfaces genéricas: IEnumerable<Object> y IEnumerable<String> . La interfaz IEnumerable<String> no hereda la
interfaz IEnumerable<Object> . En cambio, el tipo String hereda el tipo Object , y en algunos casos puede que
quiera asignar objetos de estas interfaces entre sí. Esto se muestra en el ejemplo de código siguiente.
// Comparer class.
class BaseComparer : IEqualityComparer<BaseClass>
{
public int GetHashCode(BaseClass baseInstance)
{
return baseInstance.GetHashCode();
}
public bool Equals(BaseClass x, BaseClass y)
{
return x == y;
}
}
class Program
{
static void Test()
{
IEqualityComparer<BaseClass> baseComparer = new BaseComparer();
Para obtener más ejemplos, vea Usar la varianza en interfaces para las colecciones genéricas (C#).
La varianza para interfaces genéricas solo es compatible con tipos de referencia. Los tipos de valor no admiten
la varianza. Por ejemplo, IEnumerable<int> no puede convertirse implícitamente en IEnumerable<object> ,
porque los enteros se representan mediante un tipo de valor.
También es importante recordar que las clases que implementan las interfaces variantes siguen siendo
invariables. Por ejemplo, aunque List<T> implementa la interfaz covariante IEnumerable<T>, no puede
convertir List<String> en List<Object> implícitamente. Esto se muestra en el siguiente código de ejemplo.
Vea también
Usar la varianza en interfaces para las colecciones genéricas (C#)
Crear interfaces genéricas variantes (C#)
Interfaces genéricas
Varianza en delegados (C#)
Crear interfaces genéricas variantes (C#)
16/09/2021 • 5 minutes to read
Puede declarar parámetros de tipo genérico en las interfaces como covariantes o contravariantes. La covarianza
permite que los métodos de interfaz tengan tipos de valor devuelto más derivados que los que se definen en los
parámetros de tipo genérico. La contravarianza permite que los métodos de interfaz tengan tipos de argumento
menos derivados que los que se especifican en los parámetros genéricos. Las interfaces genéricas que tienen
parámetros de tipo genérico covariantes o contravariantes se llaman variantes.
NOTE
En .NET Framework 4 se ha presentado la compatibilidad con la varianza para varias interfaces genéricas existentes. Para
ver la lista de interfaces variantes de .NET, vea Varianza en interfaces genéricas (C#).
IMPORTANT
Los parámetros ref , in y out de C# no pueden ser variantes. Los tipos de valor tampoco admiten la varianza.
Puede declarar un parámetro de tipo genérico covariante mediante la palabra clave out . El tipo covariante debe
cumplir las siguientes condiciones:
El tipo se usa únicamente como tipo de valor devuelto de los métodos de interfaz, y no como tipo de los
argumentos de método. Esto se muestra en el siguiente ejemplo, en el que el tipo R se declara como
covariante.
Hay una excepción para esta regla. Si tiene un delegado genérico contravariante como parámetro de
método, puede usar el tipo como parámetro de tipo genérico para el delegado. Esto se muestra en el
siguiente ejemplo con el tipo R . Para obtener más información, vea Varianza en delegados (C#) y Usar la
varianza para los delegados genéricos Func y Action (C#).
El tipo no se usa como restricción genérica para los métodos de interfaz. Esto se muestra en el siguiente
código.
interface ICovariant<out R>
{
// The following statement generates a compiler error
// because you can use only contravariant or invariant types
// in generic constraints.
// void DoSomething<T>() where T : R;
}
Puede declarar un parámetro de tipo genérico contravariante mediante la palabra clave in . El tipo
contravariante solo se puede usar como tipo de los argumentos de método, y no como tipo de valor devuelto de
los métodos de interfaz. El tipo contravariante también se puede usar para las restricciones genéricas. En el
siguiente código se muestra cómo declarar una interfaz contravariante y cómo usar una restricción genérica
para uno de sus métodos.
También se puede admitir la covarianza y la contravarianza en la misma interfaz, pero para distintos parámetros
de tipo, como se muestra en el siguiente ejemplo de código.
Las clases que implementan interfaces variantes son invariables. Por ejemplo, considere el fragmento de código
siguiente:
// The interface is covariant.
ICovariant<Button> ibutton = new SampleImplementation<Button>();
ICovariant<Object> iobj = ibutton;
Pero si un parámetro de tipo genérico T se declara como covariante en una interfaz, no puede declararlo como
contravariante en la interfaz extensible (o viceversa). Esto se muestra en el siguiente código de ejemplo.
Evitar la ambigüedad
Al implementar interfaces genéricas variantes, la varianza a veces puede implicar ambigüedad. Debe evitarse
esta ambigüedad.
Por ejemplo, si implementa explícitamente en una clase la misma interfaz genérica variante con distintos
parámetros de tipo genérico, puede crear ambigüedad. El compilador no genera ningún error en este caso, pero
no se especifica qué implementación de interfaz se va a elegir en tiempo de ejecución. Esta ambigüedad podría
provocar errores sutiles en el código. Observe el siguiente ejemplo de código.
// Simple class hierarchy.
class Animal { }
class Cat : Animal { }
class Dog : Animal { }
IEnumerator IEnumerable.GetEnumerator()
{
// Some code.
return null;
}
IEnumerator<Dog> IEnumerable<Dog>.GetEnumerator()
{
Console.WriteLine("Dog");
// Some code.
return null;
}
}
class Program
{
public static void Test()
{
IEnumerable<Animal> pets = new Pets();
pets.GetEnumerator();
}
}
En este ejemplo no se especifica cómo elige el método pets.GetEnumerator entre Cat y Dog . Esto podría
producir problemas en el código.
Vea también
Varianza en interfaces genéricas (C#)
Usar varianza para los delegados genéricos Func y Action (C#)
Usar la varianza en interfaces para las colecciones
genéricas (C#)
16/09/2021 • 2 minutes to read
Una interfaz covariante permite que sus métodos devuelvan tipos más derivados que los especificados en la
interfaz. Una interfaz contravariante permite que sus métodos acepten parámetros de tipos menos derivados
que los especificados en la interfaz.
Varias interfaces existentes en .NET Framework 4 pasaron a ser covariantes y contravariantes. Por ejemplo,
IEnumerable<T> y IComparable<T>. Esto permite volver a usar métodos que funcionan con colecciones
genéricas de tipos base para colecciones de tipos derivados.
Para ver una lista de interfaces variantes de .NET, vea Varianza en interfaces genéricas (C#).
class Program
{
// The method has a parameter of the IEnumerable<Person> type.
public static void PrintFullName(IEnumerable<Person> persons)
{
foreach (Person person in persons)
{
Console.WriteLine("Name: {0} {1}",
person.FirstName, person.LastName);
}
}
PrintFullName(employees);
}
}
class Program
{
IEnumerable<Employee> noduplicates =
employees.Distinct<Employee>(new PersonComparer());
Vea también
Varianza en interfaces genéricas (C#)
Varianza en delegados (C#)
16/09/2021 • 6 minutes to read
En .NET Framework 3.5 se presentó por primera vez la compatibilidad con la varianza para hacer coincidir
firmas de método con tipos de delegados en todos los delegados en C#. Esto significa que puede asignar a los
delegados no solo métodos con firmas coincidentes, sino métodos que devuelven tipos más derivados
(covarianza) o que aceptan parámetros con tipos menos derivados (contravarianza) que el especificado por el
tipo de delegado. Esto incluye delegados genéricos y no genéricos.
Por ejemplo, consideremos el siguiente código, que tiene dos clases y dos delegados: genéricos y no genéricos.
Al crear delegados de los tipos SampleDelegate o SampleGenericDelegate<A, R> , puede asignar uno de los
métodos siguientes a dichos delegados.
// Matching signature.
public static First ASecondRFirst(Second second)
{ return new First(); }
En el ejemplo de código siguiente se ilustra la conversión implícita entre la firma del método y el tipo de
delegado.
Si usa solo la compatibilidad con la varianza para hacer coincidir firmas de método con tipos de delegados y no
usa las palabras clave in y out , es posible que en algunas ocasiones pueda crear instancias de delegados con
métodos o expresiones lambda idénticos, pero no pueda asignar un delegado a otro.
En el ejemplo de código siguiente, SampleGenericDelegate<String> no se puede convertir explícitamente a
SampleGenericDelegate<Object> , aunque String hereda Object . Para solucionar este problema, marque el
parámetro genérico T con la palabra clave out .
Puede declarar un parámetro de tipo genérico contravariante en un delegado genérico mediante la palabra
clave in . El tipo contravariante puede usarse solo como un tipo de argumentos de método, y no como un tipo
de valor devuelto de método. En el siguiente ejemplo de código se muestra cómo declarar un delegado genérico
contravariante.
IMPORTANT
Los parámetros ref , in y out de C# no se pueden marcar como variantes.
También es posible admitir la varianza y la covarianza en el mismo delegado, pero para parámetros de tipo
diferentes. Esta implementación se muestra en el ejemplo siguiente.
Vea también
Genéricos
Usar varianza para los delegados genéricos Func y Action (C#)
Procedimiento para combinar delegados (delegados de multidifusión)
Usar varianza en delegados (C#)
16/09/2021 • 2 minutes to read
Ejemplo 1: Covarianza
Descripción
En este ejemplo se muestra cómo se pueden usar delegados con métodos que tienen tipos de valor devuelto
derivados del tipo de valor devuelto en la firma del delegado. El tipo de datos devuelto por DogsHandler es de
tipo Dogs , que se deriva del tipo Mammals definido en el delegado.
Código
class Mammals {}
class Dogs : Mammals {}
class Program
{
// Define the delegate.
public delegate Mammals HandlerMethod();
Ejemplo 2: Contravarianza
Descripción
En este ejemplo se muestra cómo se pueden usar delegados con métodos que tienen parámetros que son tipos
base del tipo de parámetro de la firma del delegado. Con la contravarianza, puede usar un controlador de
eventos en lugar de controladores independientes. En el ejemplo siguiente se usan dos delegados:
Un delegado KeyEventHandler que define la firma del evento Button.KeyDown. Su firma es:
public delegate void KeyEventHandler(object sender, KeyEventArgs e)
Un delegado MouseEventHandler que define la firma del evento Button.MouseClick. Su firma es:
En el ejemplo se define un controlador de eventos con un parámetro EventArgs, que se usa para controlar los
eventos Button.KeyDown y Button.MouseClick . Es posible hacer esto porque EventArgs es un tipo base de
KeyEventArgs y MouseEventArgs.
Código
public Form1()
{
InitializeComponent();
Consulte también
Varianza en delegados (C#)
Usar varianza para los delegados genéricos Func y Action (C#)
Usar la varianza para los delegados genéricos Func
y Action (C#)
16/09/2021 • 2 minutes to read
En estos ejemplos se muestra cómo usar la covarianza y la contravarianza en los delegados genéricos Func y
Action para habilitar la reutilización de métodos y proporcionar más flexibilidad en el código.
Para obtener más información sobre la covarianza y la contravarianza, vea Varianza en delegados (C#)
}
}
Vea también
Covarianza y contravarianza (C#)
Genéricos
Árboles de expresión (C#)
16/09/2021 • 5 minutes to read
Los árboles de expresión representan el código en una estructura de datos en forma de árbol donde cada nodo
es una expresión, por ejemplo, una llamada a método o una operación binaria como x < y .
El código representado en árboles de expresión se puede compilar y ejecutar. Esto permite realizar cambios
dinámicos en el código ejecutable, ejecutar consultas LINQ en varias bases de datos y crear consultas dinámicas.
Para obtener más información sobre los árboles de expresión en LINQ, consulte Procedimiento para usar
árboles de expresión para crear consultas dinámicas (C#).
Los árboles de expresión también se usan en Dynamic Language Runtime (DLR) para proporcionar
interoperabilidad entre los lenguajes dinámicos y .NET y, asimismo, para permitir que los programadores de
compiladores emitan árboles de expresión en lugar de Lenguaje intermedio de Microsoft (MSIL). Para obtener
más información sobre el entorno DLR, vea Información general acerca de Dynamic Language Runtime.
Puede hacer que el compilador de Visual Basic o de C# cree un árbol de expresión en función de una expresión
lambda anónima, o bien puede crear árboles de expresión manualmente usando el espacio de nombres
System.Linq.Expressions.
En .NET Framework 4 y versiones posteriores, la API de árboles de expresión admite también asignaciones y
expresiones de flujo de control como bucles, bloques condicionales y bloques try-catch . Con la API, se pueden
crear árboles de expresión más complejos que los que pueden crear el compilador de C# a partir de expresiones
lambda. En el siguiente ejemplo se indica cómo crear un árbol de expresión que calcula el factorial de un
número.
Console.WriteLine(factorial);
// Prints 120.
Para obtener más información, consulte Generating Dynamic Methods with Expression Trees in Visual Studio
2010 (Generar métodos dinámicos con árboles de expresión en Visual Studio 2010), que también se aplica a las
últimas versiones de Visual Studio.
Analizar árboles de expresión
En el siguiente ejemplo de código se muestra cómo la expresión del árbol que representa la expresión lambda
num => num < 5 se puede descomponer en partes.
// Prints True.
Para obtener más información, consulte Procedimiento para ejecutar árboles de expresión (C#).
Vea también
System.Linq.Expressions
Procedimiento para ejecutar árboles de expresión (C#)
Procedimiento para modificar árboles de expresión (C#)
Expresiones lambda
Información general sobre Dynamic Language Runtime
Conceptos de programación (C#)
Procedimiento para ejecutar árboles de expresión
(C#)
16/09/2021 • 2 minutes to read
En este tema se muestra cómo ejecutar un árbol de expresión. La ejecución de un árbol de expresión puede
devolver un valor o simplemente realizar una acción, como llamar a un método.
Solo se pueden ejecutar los árboles de expresiones que representan expresiones lambda. Los árboles de
expresiones que representan expresiones lambda son de tipo LambdaExpression o Expression<TDelegate>. Para
ejecutar estos árboles de expresiones, llame al método Compile para crear un delegado ejecutable y, después,
invoque el delegado.
NOTE
Si el tipo del delegado es desconocido, es decir, la expresión lambda es de tipo LambdaExpression y no
Expression<TDelegate>, debe llamar al método DynamicInvoke en el delegado en lugar de invocarlo directamente.
Si un árbol de expresión no representa una expresión lambda, puede crear una nueva expresión lambda que
tenga el árbol de expresión original como su cuerpo llamando al método Lambda<TDelegate>(Expression,
IEnumerable<ParameterExpression>). Luego puede ejecutar la expresión lambda tal y como se ha descrito
anteriormente en esta sección.
Ejemplo
En el ejemplo de código siguiente se muestra cómo ejecutar un árbol de expresión que representa la elevación
de un número a una potencia mediante la creación de una expresión lambda y su posterior ejecución. Se
muestra el resultado, que representa el número elevado a la potencia.
Compilar el código
Incluya el espacio de nombres System.Linq.Expressions.
Vea también
Árboles de expresión (C#)
Procedimiento para modificar árboles de expresión (C#)
Procedimiento para modificar árboles de expresión
(C#)
16/09/2021 • 2 minutes to read
En este tema se muestra cómo modificar un árbol de expresión. Los árboles de expresiones son inmutables, lo
que significa que no pueden modificarse directamente. Para cambiar un árbol de expresión, debe crear una
copia de un árbol de expresión existente y, una vez creada la copia, realizar los cambios necesarios. Puede usar
la clase ExpressionVisitor para recorrer un árbol de expresión existente y copiar cada nodo que visita.
Para modificar un árbol de expresión
1. Cree un nuevo proyecto de aplicación de consola .
2. Agregue una directiva using al archivo para el espacio de nombres System.Linq.Expressions .
3. Agregue la clase AndAlsoModifier al proyecto.
return base.VisitBinary(b);
}
}
Esta clase hereda la clase ExpressionVisitor y está especializada en la modificación de expresiones que
representan operaciones AND condicionales. Cambia estas operaciones de una expresión AND
condicional a una expresión OR condicional. Para ello, la clase invalida el método VisitBinary del tipo
base, porque las expresiones AND condicionales se representan como expresiones binarias. En el método
VisitBinary , si la expresión que se pasa representa una operación AND condicional, el código construye
una nueva expresión que contiene el operador OR condicional en lugar del operador AND condicional. Si
la expresión que se pasa a VisitBinary no representa una operación AND condicional, el método defiere
a la implementación de la case base. Los métodos de clase base construyen nodos que son como los
árboles de expresiones que se pasan, pero los subárboles de los nodos se reemplazan por los árboles de
expresiones que genera de forma recursiva el visitante.
4. Agregue una directiva using al archivo para el espacio de nombres System.Linq.Expressions .
5. Agregue código al método Main en el archivo Program.cs para crear un árbol de expresión y pasarlo al
método que lo modificará.
Expression<Func<string, bool>> expr = name => name.Length > 10 && name.StartsWith("G");
Console.WriteLine(expr);
Console.WriteLine(modifiedExpr);
El código crea una expresión que contiene una operación AND condicional. Luego crea una instancia de la
clase AndAlsoModifier y pasa la expresión al método Modify de esta clase. Se generan los árboles de
expresiones tanto originales como modificados para mostrar el cambio.
6. Compile y ejecute la aplicación.
Vea también
Procedimiento para ejecutar árboles de expresión (C#)
Árboles de expresión (C#)
Consultas basadas en el estado del entorno de
ejecución (C#)
16/09/2021 • 9 minutes to read
Tenga en cuenta el código que define una interfaz IQueryable o IQueryable<T> con respecto a un origen de
datos:
// We're using an in-memory array as the data source, but the IQueryable could have come
// from anywhere -- an ORM backed by a database, a web request, or any other LINQ provider.
IQueryable<string> companyNamesSource = companyNames.AsQueryable();
var fixedQry = companyNames.OrderBy(x => x);
Cada vez que ejecute este código, se ejecutará la misma consulta exacta. Esto no suele ser muy útil, ya que es
posible que quiera que el código ejecute otras consultas en función de las condiciones en el momento de la
ejecución. En este artículo se describe cómo puede ejecutar otra consulta en función del estado del entorno de
ejecución.
var length = 1;
var qry = companyNamesSource
.Select(x => x.Substring(0, length))
.Distinct();
Console.WriteLine(string.Join(",", qry));
// prints: C, A, S, W, G, H, M, N, B, T, L, F
length = 2;
Console.WriteLine(string.Join(",", qry));
// prints: Co, Al, So, Ci, Wi, Gr, Ad, Hu, Wo, Ma, No, Bl, Tr, Th, Lu, Fo
El árbol de expresión interno (y, por tanto, la consulta) no se ha modificado; la consulta devuelve otros valores
solo porque se ha cambiado el valor de length .
También es posible que quiera crear las distintas subexpresiones mediante una biblioteca de terceros, como
PredicateBuilder de LinqKit:
// using LinqKit;
// string? startsWith = /* ... */;
// string? endsWith = /* ... */;
Los pasos básicos para crear una instancia de Expression<TDelegate> son los siguientes:
Defina objetos ParameterExpression para cada uno de los parámetros (si existen) de la expresión lambda,
mediante el método generador Parameter.
Encapsule los parámetros y el cuerpo en una instancia de Expression<TDelegate> con tipo de tiempo de
compilación, mediante la sobrecarga correspondiente del método de generador Lambda:
En las secciones siguientes se describe un escenario en el que es posible que quiera crear una instancia de
Expression<TDelegate> para pasarla a un método de LINQ, y se proporciona un ejemplo completo de cómo
hacerlo mediante los métodos de generador.
Escenario
Imagine que tiene varios tipos de entidad:
En cualquiera de estos tipos de entidad, quiere filtrar y devolver solo las entidades que contengan un texto
concreto dentro de uno de sus campos string . Para Person , le interesa buscar las propiedades FirstName y
LastName :
Aunque podría escribir una función personalizada para IQueryable<Person> y otra para IQueryable<Car> , la
siguiente función agrega este filtrado a cualquier consulta existente, con independencia del tipo de elemento
específico.
Ejemplo
// Because the lambda is compile-time-typed (albeit with a generic parameter), we can use it with the
Where method
return source.Where(lambda);
}
Como la función TextFilter toma y devuelve una interfaz IQueryable<T> (y no solo una interfaz IQueryable),
puede agregar más elementos de consulta con tipo de tiempo de compilación después del filtro de texto.
// The logic for building the ParameterExpression and the LambdaExpression's body is the same as in the
previous example,
// but has been refactored into the constructBody function.
(Expression? body, ParameterExpression? prm) = constructBody(elementType, term);
if (body is null) {return source;}
return source.Provider.CreateQuery(filteredTree);
}
Tenga en cuenta que en este caso no tiene un marcador de posición genérico T en tiempo de compilación, por
lo que usará la sobrecarga de Lambda que no necesita información de tipos de tiempo de compilación y que
genera un elemento LambdaExpression en lugar de una instancia de Expression<TDelegate>.
// using System.Linq.Dynamic.Core
Vea también
Árboles de expresión (C#)
Procedimiento para ejecutar árboles de expresión (C#)
Especificar dinámicamente filtros con predicado en tiempo de ejecución
Depuración de árboles de expresión en Visual
Studio (C#)
16/09/2021 • 2 minutes to read
Se puede analizar la estructura y el contenido de los árboles de expresión cuando se depuran las aplicaciones.
Para obtener una introducción rápida a la estructura del árbol de expresión, puede usar la propiedad DebugView ,
que representa los árboles de expresión con una sintaxis especial. Observe que DebugView solo está disponible
en modo de depuración.
Puesto que DebugView es una cadena, puede usar el visualizador de texto integrado para verla en varias líneas si
selecciona Visualizador de texto en el icono de lupa situado junto a la etiqueta DebugView .
Como alternativa, puede instalar y usar un visualizador personalizado para árboles de expresión, como:
Readable Expressions (licencia MIT, disponible en Visual Studio Marketplace), representa el árbol de
expresión como código de C# con temas, con varias opciones de representación:
Expression Tree Visualizer (licencia MIT), proporciona una vista de árbol del árbol de expresión y sus
nodos individuales:
Para abrir un visualizador para un árbol de expresión
1. Haga clic en el icono de lupa que aparece junto al árbol de expresión en Información sobre datos , en
una ventana Inspección o en las ventanas Automático o Variables locales .
Se muestra una lista de los visualizadores disponibles:
Vea también
Árboles de expresión (C#)
Depurar en Visual Studio
Create Custom Visualizers (Crear visualizadores personalizados)
Sintaxis DebugView
Sintaxis de DebugView
16/09/2021 • 2 minutes to read
La propiedad DebugView (disponible solo durante la depuración) proporciona una representación de cadenas
de árboles de expresión. La mayor parte de la sintaxis es bastante sencilla de entender; los casos especiales se
describen en las siguientes secciones.
Cada ejemplo va seguido de un comentario del bloque, que contiene DebugView .
ParameterExpression
los nombres de variable ParameterExpression se muestran con un símbolo $ al principio.
Si un parámetro no tiene un nombre, se le asigna un nombre generado automáticamente, como $var1 o
$var2 .
Ejemplos
ConstantExpression
Para los objetos ConstantExpression que representan valores enteros, cadenas y null , se muestra el valor de la
constante.
Para los tipos numéricos que tienen sufijos estándar como literales de C#, el sufijo se agrega al valor. En la tabla
siguiente se muestran los sufijos asociados a varios tipos numéricos.
System.UInt32 uint U
System.Int64 long L
System.UInt64 ulong UL
System.Double double D
System.Single float F
System.Decimal decimal M
Ejemplos
int num = 10;
ConstantExpression expr = Expression.Constant(num);
/*
10
*/
BlockExpression
Si el tipo de un objeto BlockExpression difiere del tipo de la última expresión del bloque, el tipo se muestra entre
corchetes angulares ( < y > ). De otro modo, el tipo del objeto BlockExpression no se muestra.
Ejemplos
LambdaExpression
Los objetos LambdaExpression se muestran junto con sus tipos delegados.
Si una expresión lambda no tiene un nombre, se le asigna un nombre generado automáticamente, como
#Lambda1 o #Lambda2 .
Ejemplos
LabelExpression
Si especifica un valor predeterminado para el objeto LabelExpression, este valor se muestra antes del objeto
LabelTarget.
El token .Label indica el inicio de la etiqueta. El token .LabelTarget indica el destino al que se va a saltar.
Si una etiqueta no tiene un nombre, se le asigna un nombre generado automáticamente, como #Label1 o
#Label2 .
Ejemplos
Operadores activados
Los operadores activados se muestran con el símbolo # delante del operador. Por ejemplo, el operador de
adición activado se muestra como #+ .
Ejemplos
El tipo de valor devuelto de un método de iterador o descriptor de acceso get puede ser IEnumerable,
IEnumerable<T>, IEnumerator o IEnumerator<T>.
Puede usar una instrucción yield break para finalizar la iteración.
NOTE
En todos los ejemplos de este tema, excepto en el ejemplo de iterador simple, incluya directivas using para los espacios de
nombres System.Collections y System.Collections.Generic .
Iterador simple
El ejemplo siguiente tiene una única instrucción yield return que está dentro de un bucle for. En Main , cada
iteración del cuerpo de la instrucción foreach crea una llamada a la función de iterador, que continúa a la
instrucción yield return siguiente.
static void Main()
{
foreach (int number in EvenSequence(5, 18))
{
Console.Write(number.ToString() + " ");
}
// Output: 6 8 10 12 14 16 18
Console.ReadKey();
}
En el ejemplo siguiente se crea una clase Zoo que contiene una colección de animales.
La instrucción foreach que hace referencia a la instancia de clase ( theZoo ) llama implícitamente al método
GetEnumerator . Las instrucciones foreach que hacen referencia a las propiedades Birds y Mammals usan el
método iterador con el nombre AnimalsForType .
static void Main()
{
Zoo theZoo = new Zoo();
theZoo.AddMammal("Whale");
theZoo.AddMammal("Rhinoceros");
theZoo.AddBird("Penguin");
theZoo.AddBird("Warbler");
Console.ReadKey();
}
// Public methods.
public void AddMammal(string name)
{
animals.Add(new Animal { Name = name, Type = Animal.TypeEnum.Mammal });
}
// Public members.
public IEnumerable Mammals
{
get { return AnimalsForType(Animal.TypeEnum.Mammal); }
}
// Private methods.
// Private methods.
private IEnumerable AnimalsForType(Animal.TypeEnum type)
{
foreach (Animal theAnimal in animals)
{
if (theAnimal.Type == type)
{
yield return theAnimal.Name;
}
}
}
// Private class.
private class Animal
{
public enum TypeEnum { Bird, Mammal }
Console.ReadKey();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
Implementación técnica
Aunque un iterador se escribe como un método, el compilador lo traduce a una clase anidada que es, en
realidad, una máquina de estados. Esta clase realiza el seguimiento de la posición del iterador mientras el bucle
foreach continúe en el código de cliente.
Para ver lo que hace el compilador, puede usar la herramienta Ildasm.exe para ver el código de lenguaje
intermedio de Microsoft que se genera para un método de iterador.
Cuando crea un iterador para una clase o struct, no necesita implementar la interfaz IEnumerator completa.
Cuando el compilador detecta el iterador, genera automáticamente los métodos Current , MoveNext y Dispose
de la interfaz IEnumerator o IEnumerator<T>.
En cada iteración sucesiva del bucle foreach (o la llamada directa a IEnumerator.MoveNext ), el cuerpo de código
del iterador siguiente se reanuda después de la instrucción yield return anterior. Después continúa con la
siguiente instrucción yield return hasta que se alcanza el final del cuerpo del iterador, o hasta que se
encuentra una instrucción yield break .
Los iteradores no admiten el método IEnumerator.Reset. Para volver a recorrer en iteración desde el principio, se
debe obtener un nuevo iterador. Una llamada a Reset en el iterador devuelto por un método de iterador inicia
una excepción NotSupportedException.
Para obtener más información, vea la Especificación del lenguaje C#.
Uso de iteradores
Los iteradores permiten mantener la simplicidad de un bucle foreach cuando se necesita usar código complejo
para rellenar una secuencia de lista. Esto puede ser útil si quiere hacer lo siguiente:
Modificar la secuencia de lista después de la primera iteración del bucle foreach .
Evitar que se cargue totalmente una lista grande antes de la primera iteración de un bucle foreach . Un
ejemplo es una búsqueda paginada para cargar un lote de filas de tabla. Otro ejemplo es el método
EnumerateFiles, que implementa iteradores en .NET.
Encapsular la creación de la lista en el iterador. En el método iterador, puede crear la lista y después
devolver cada resultado en un bucle.
Vea también
System.Collections.Generic
IEnumerable<T>
foreach, in
yield
Utilizar foreach con matrices
Genéricos
Language Integrated Query (LINQ) (C#)
16/09/2021 • 4 minutes to read
class LINQQueryExpressions
{
static void Main()
{
En la siguiente ilustración de Visual Studio se muestra una consulta LINQ parcialmente completa en una base de
datos de SQL Server en C# y Visual Basic con la comprobación de tipos completa y la compatibilidad con
IntelliSense:
Información general sobre la expresión de consulta
Las expresiones de consulta se pueden utilizar para consultar y transformar los datos de cualquier origen de
datos habilitado para LINQ. Por ejemplo, una sola consulta puede recuperar datos de una base de datos SQL
y generar una secuencia XML como salida.
Las expresiones de consulta son fáciles de controlar porque utilizan muchas construcciones de lenguaje C#
familiares.
Todas las variables de una expresión de consulta están fuertemente tipadas, aunque en muchos casos no es
necesario proporcionar el tipo explícitamente porque el compilador puede deducirlo. Para más información,
vea Relaciones entre tipos en operaciones de consulta LINQ.
Una consulta no se ejecuta hasta que no se realiza la iteración a través de la variable de consulta, por
ejemplo, en una instrucción foreach . Para más información, vea Introducción a las consultas LINQ.
En tiempo de compilación, las expresiones de consulta se convierten en llamadas al método de operador de
consulta estándar según las reglas establecidas en la especificación de C#. Cualquier consulta que se puede
expresar con sintaxis de consulta también se puede expresar mediante sintaxis de método. Sin embargo, en
la mayoría de los casos, la sintaxis de consulta es más legible y concisa. Para más información, vea
Especificación del lenguaje C# e Información general sobre operadores de consulta estándar.
Como regla al escribir consultas LINQ, se recomienda utilizar la sintaxis de consulta siempre que sea posible
y la sintaxis de método cuando sea necesario. No hay diferencias semánticas ni de rendimiento entre estas
dos formas diversas. Las expresiones de consulta suelen ser más legibles que las expresiones equivalentes
escritas con la sintaxis de método.
Algunas operaciones de consulta, como Count o Max, no tienen ninguna cláusula de expresión de consulta
equivalente, de modo que deben expresarse como una llamada de método. La sintaxis de método se puede
combinar con la sintaxis de consulta de varias maneras. Para más información, vea Sintaxis de consultas y
sintaxis de métodos en LINQ.
Las expresiones de consulta pueden compilarse en árboles de expresión o en delegados, en función del tipo
al que se aplica la consulta. Las consultas IEnumerable<T> se compilan en delegados. Las consultas
IQueryable y IQueryable<T> se compilan en árboles de expresión. Para más información, vea Árboles de
expresión.
Pasos siguientes
Para obtener más información sobre LINQ, empiece a familiarizarse con algunos conceptos básicos en
Conceptos básicos de las expresiones de consultas y, después, lea la documentación de la tecnología de LINQ en
la que esté interesado:
Documentos XML: LINQ to XML
ADO.NET Entity Framework: LINQ to Entities
Colecciones .NET, archivos y cadenas, entre otros: LINQ to Objects
Para comprender mejor los aspectos generales de LINQ, vea LINQ in C# (LINQ en C#).
Para empezar a trabajar con LINQ en C#, vea el tutorial Trabajar con LINQ.
Introducción a las consultas LINQ (C#)
16/09/2021 • 6 minutes to read
Una consulta es una expresión que recupera datos de un origen de datos. Las consultas se suelen expresar en un
lenguaje de consultas especializado. Con el tiempo se han desarrollado diferentes lenguajes para los distintos
tipos de orígenes de datos, como SQL para las bases de datos relacionales y XQuery para XML. Por lo tanto, los
programadores han tenido que aprender un lenguaje de consultas nuevo para cada tipo de origen de datos o
formato de datos que deben admitir. LINQ simplifica esta situación al ofrecer un modelo coherente para trabajar
con los datos de varios formatos y orígenes. En una consulta LINQ siempre se trabaja con objetos. Se usan los
mismos patrones de codificación básicos para consultar y transformar datos de documentos XML, bases de
datos SQL, conjuntos de datos de ADO.NET, colecciones de .NET y cualquier otro formato para el que haya
disponible un proveedor de LINQ.
class IntroToLINQ
{
static void Main()
{
// The Three Parts of a LINQ Query:
// 1. Data source.
int[] numbers = new int[7] { 0, 1, 2, 3, 4, 5, 6 };
// 2. Query creation.
// numQuery is an IEnumerable<int>
var numQuery =
from num in numbers
where (num % 2) == 0
select num;
// 3. Query execution.
foreach (int num in numQuery)
{
Console.Write("{0,1} ", num);
}
}
}
Con LINQ to SQL, primero se crea una asignación relacional de objetos en tiempo de diseño ya sea
manualmente o mediante las herramientas de LINQ to SQL en Visual Studio. Después, se escriben las consultas
en los objetos y, en tiempo de ejecución, LINQ to SQL controla la comunicación con la base de datos. En el
ejemplo siguiente, Customers representa una tabla específica en una base de datos, y el tipo del resultado de la
consulta, IQueryable<T>, se deriva de IEnumerable<T>.
Para obtener más información sobre cómo crear tipos específicos de orígenes de datos, consulte la
documentación de los distintos proveedores de LINQ. Aun así, la regla básica es muy sencilla: un origen de datos
de LINQ es cualquier objeto que admita la interfaz genérica IEnumerable<T> o una interfaz que la haya
heredado.
NOTE
Los tipos como ArrayList, que admiten la interfaz no genérica IEnumerable, también se pueden usar como origen de datos
de LINQ. Para más información, consulte el procedimiento para consultar un objeto ArrayList con LINQ (C#).
La consulta
La consulta especifica la información que se debe recuperar de los orígenes de datos. Opcionalmente, una
consulta también especifica cómo se debe ordenar, agrupar y conformar esa información antes de que se
devuelva. Las consultas se almacenan en una variable de consulta y se inicializan con una expresión de consulta.
Para facilitar la escritura de consultas, C# ha incorporado una nueva sintaxis de consulta.
La consulta del ejemplo anterior devuelve todos los números pares de la matriz de enteros. La expresión de
consulta contiene tres cláusulas: from , where y select (si está familiarizado con SQL, habrá observado que el
orden de las cláusulas se invierte respecto al orden de SQL). La cláusula from especifica el origen de datos, la
cláusula where aplica el filtro y la cláusula select especifica el tipo de los elementos devueltos. Estas y otras
cláusulas de consulta se tratan con detalle en la sección Language Integrated Query (LINQ). Por ahora, lo
importante es que en LINQ la variable de consulta no efectúa ninguna acción y no devuelve ningún dato. Lo
único que hace es almacenar la información necesaria para generar los resultados cuando se ejecuta la consulta
en algún momento posterior. Para obtener más información sobre cómo se construyen las consultas en
segundo plano, vea Información general sobre operadores de consulta estándar (C#).
NOTE
Las consultas también se pueden expresar empleando una sintaxis de método. Para obtener más información, vea Query
Syntax and Method Syntax in LINQ (Sintaxis de consulta y sintaxis de método en LINQ).
Ejecución de la consulta
Ejecución aplazada
Como se ha indicado anteriormente, la variable de consulta solo almacena los comandos de consulta. La
ejecución real de la consulta se aplaza hasta que se procese una iteración en la variable de consulta en una
instrucción foreach . Este concepto se conoce como ejecución aplazada y se muestra en el ejemplo siguiente:
// Query execution.
foreach (int num in numQuery)
{
Console.Write("{0,1} ", num);
}
La instrucción foreach es también donde se recuperan los resultados de la consulta. Por ejemplo, en la consulta
anterior, la variable de iteración num contiene cada valor (de uno en uno) en la secuencia devuelta.
Dado que la propia variable de consulta nunca contiene los resultados de la consulta, puede ejecutarla tantas
veces como desee. Por ejemplo, se puede tener una base de datos que esté siendo actualizada de forma
continua por otra aplicación. En su aplicación, se puede crear una consulta que recupere los datos más recientes
y se puede ejecutar repetidamente de acuerdo con un intervalo para recuperar resultados diferentes cada vez.
Forzar la ejecución inmediata
Las consultas que llevan a cabo funciones de agregación en un intervalo de elementos de origen primero deben
recorrer en iteración dichos elementos. Ejemplos de estas consultas son Count , Max , Average y First . Se
ejecutan sin una instrucción foreach explícita, ya que la propia consulta debe usar foreach para poder
devolver un resultado. Tenga en cuenta también que estos tipos de consultas devuelven un único valor, y no una
colección IEnumerable . La consulta siguiente devuelve un recuento de los números pares de la matriz de origen:
var evenNumQuery =
from num in numbers
where (num % 2) == 0
select num;
Para forzar la ejecución inmediata de cualquier consulta y almacenar en caché los resultados correspondientes,
puede llamar a los métodos ToList o ToArray.
List<int> numQuery2 =
(from num in numbers
where (num % 2) == 0
select num).ToList();
// or like this:
// numQuery3 is still an int[]
var numQuery3 =
(from num in numbers
where (num % 2) == 0
select num).ToArray();
También puede forzar la ejecución colocando el bucle foreach justo después de la expresión de consulta,
aunque, si se llama a ToList o a ToArray , también se almacenan en caché todos los datos de un objeto de
colección.
Vea también
Introducción a LINQ en C#
Tutorial: Escribir consultas en C#
Language-Integrated Query (LINQ)
foreach, in
Palabras clave para consultas (LINQ)
LINQ y tipos genéricos (C#)
16/09/2021 • 2 minutes to read
Las consultas LINQ se basan en tipos genéricos incorporados en la versión 2.0 de .NET Framework. No es
necesario tener conocimientos avanzados de genéricos para poder empezar a escribir consultas, aunque debería
entender dos conceptos básicos:
1. Al crear una instancia de una clase de colección genérica como List<T>, reemplace la "T" por el tipo de
objetos que contendrá la lista. Por ejemplo, una lista de cadenas se expresa como List<string> y una
lista de objetos Customer se expresa como List<Customer> . Las listas genéricas están fuertemente
tipadas y ofrecen muchas ventajas respecto a las colecciones que almacenan sus elementos como Object.
Si intenta agregar un Customer a una List<string> , se producirá un error en tiempo de compilación.
Usar colecciones genéricas es fácil porque no es necesario efectuar ninguna conversión de tipos en
tiempo de ejecución.
2. IEnumerable<T> es la interfaz que permite enumerar las clases de colección genéricas mediante la
instrucción foreach . Las clases de colección genéricas admiten IEnumerable<T> simplemente como
clases de colección no genéricas como ArrayList admite IEnumerable.
Para obtener más información sobre los genéricos, vea Genéricos.
IEnumerable<Customer> customerQuery =
from cust in customers
where cust.City == "London"
select cust;
Para obtener más información, vea Relaciones entre tipos en operaciones de consulta LINQ.
La palabra clave var es útil cuando el tipo de la variable es obvio o cuando no es tan importante especificar
explícitamente los tipos genéricos anidados, como los que generan las consultas de grupo. Le recordamos que,
si usa var , debe tener presente que puede dificultar la lectura del código a otros usuarios. Para más
información, vea Variables locales con asignación implícita de tipos.
Vea también
Genéricos
Operaciones básicas de consulta LINQ (C#)
16/09/2021 • 5 minutes to read
En este tema se ofrece una breve introducción a las expresiones de consulta LINQ y algunas de las clases de
operaciones típicas que se realizan en una consulta. En los temas siguientes se ofrece información más
detallada:
Expresiones de consulta LINQ
Información general sobre operadores de consulta estándar (C#)
Tutorial: Creación de consultas en C#
NOTE
Si ya está familiarizado con un lenguaje de consultas como SQL o XQuery, puede omitir la mayoría de este tema. Lea la
parte dedicada a la "cláusula from " en la sección siguiente para obtener información sobre el orden de las cláusulas en
las expresiones de consulta LINQ.
//queryAllCustomers is an IEnumerable<Customer>
var queryAllCustomers = from cust in customers
select cust;
La variable de rango es como la variable de iteración en un bucle foreach , con la diferencia de que en una
expresión de consulta realmente no se produce ninguna iteración. Cuando se ejecuta la consulta, la variable de
rango actúa como referencia para cada elemento sucesivo de customers . Dado que el compilador puede
deducir el tipo de cust , no tiene que especificarlo explícitamente. Una cláusula let puede introducir variables
de rango adicionales. Para obtener más información, vea let (Cláusula).
NOTE
Para los orígenes de datos no genéricos, como ArrayList, el tipo de la variable de rango debe establecerse explícitamente.
Para más información, consulte el procedimiento para consultar un objeto ArrayList con LINQ (C#) y Cláusula from.
Filtrado
Probablemente la operación de consulta más común es aplicar un filtro en forma de expresión booleana. El filtro
hace que la consulta devuelva solo los elementos para los que la expresión es verdadera. El resultado se genera
mediante la cláusula where . El filtro aplicado especifica qué elementos se deben excluir de la secuencia de
origen. En el ejemplo siguiente, solo se devuelven los customers cuya dirección se encuentra en Londres.
var queryLondonCustomers = from cust in customers
where cust.City == "London"
select cust;
Puede usar los operadores lógicos AND y OR de C#, con los que ya estará familiarizado, para aplicar las
expresiones de filtro que sean necesarias en la cláusula where . Por ejemplo, para devolver solo los clientes con
dirección en "London" AND cuyo nombre sea "Devon", escribiría el código siguiente:
Para devolver los clientes con dirección en Londres o París, escribiría el código siguiente:
Ordenación
A menudo es necesario ordenar los datos devueltos. La cláusula orderby hará que los elementos de la
secuencia devuelta se ordenen según el comparador predeterminado del tipo que se va a ordenar. Por ejemplo,
la consulta siguiente se puede extender para ordenar los resultados según la propiedad Name . Dado que Name
es una cadena, el comparador predeterminado realiza una ordenación alfabética de la A a la Z.
var queryLondonCustomers3 =
from cust in customers
where cust.City == "London"
orderby cust.Name ascending
select cust;
Agrupar
La cláusula group permite agrupar los resultados según la clave que se especifique. Por ejemplo, podría
especificar que los resultados se agrupen por City para que todos los clientes de London o París estén en
grupos individuales. En este caso, la clave es cust.City .
Combinación
Las operaciones de combinación crean asociaciones entre las secuencias que no se modelan explícitamente en
los orígenes de datos. Por ejemplo, puede realizar una combinación para buscar todos los clientes y
distribuidores que tengan la misma ubicación. En LINQ, la cláusula join funciona siempre con colecciones de
objetos, en lugar de con tablas de base de datos directamente.
var innerJoinQuery =
from cust in customers
join dist in distributors on cust.City equals dist.City
select new { CustomerName = cust.Name, DistributorName = dist.Name };
En LINQ no es necesario usar join tan a menudo como en SQL, porque las claves externas en LINQ se
representan en el modelo de objetos como propiedades que contienen una colección de elementos. Por
ejemplo, un objeto Customer contiene una colección de objetos Order . En lugar de realizar una combinación,
tiene acceso a los pedidos usando la notación de punto:
Selección (proyecciones)
La cláusula select genera resultados de consulta y especifica la "forma" o el tipo de cada elemento devuelto.
Por ejemplo, puede especificar si sus resultados estarán compuestos de objetos Customer completos, un solo
miembro, un subconjunto de miembros o algún tipo de resultado completamente diferente basado en un
cálculo o en un objeto nuevo. Cuando la cláusula select genera algo distinto de una copia del elemento de
origen, la operación se denomina proyección. El uso de proyecciones para transformar los datos es una función
eficaz de las expresiones de consulta LINQ. Para obtener más información, vea Transformaciones de datos con
LINQ (C#) y select (cláusula).
Consulte también
Expresiones de consulta LINQ
Tutorial: Creación de consultas en C#
Palabras clave para consultas (LINQ)
Tipos anónimos
Transformaciones de datos con LINQ (C#)
16/09/2021 • 6 minutes to read
Language-Integrated Query (LINQ) no solo es para recuperar datos. También es una herramienta eficaz para
transformarlos. Mediante el uso de un consulta LINQ, se puede usar una secuencia de origen como entrada y
modificarla de muchas maneras para crear una nueva secuencia de salida. Por medio de ordenaciones y
agrupaciones se puede modificar la propia secuencia sin modificar los elementos. Pero quizás la característica
más eficaz de las consultas LINQ es la capacidad para crear nuevos tipos. Esto se realiza en la cláusula select. Por
ejemplo, puede realizar las tareas siguientes:
Combinar varias secuencias de entrada en una sola secuencia de salida que tiene un tipo nuevo.
Crear secuencias de salida cuyos elementos estén formados por una o varias propiedades de cada
elemento de la secuencia de origen.
Crear secuencias de salida cuyos elementos estén formados por los resultados de las operaciones
realizadas en el origen de datos.
Crear secuencias de salida en un formato diferente. Por ejemplo, se pueden transformar datos de filas de
SQL o archivos de texto en XML.
Estos son solo algunos ejemplos. Por supuesto, estas transformaciones pueden combinarse de diversas formas
en la misma consulta. Además, se puede usar la secuencia de salida de una consulta como la secuencia de
entrada para una nueva consulta.
class Student
{
public string First { get; set; }
public string Last {get; set;}
public int ID { get; set; }
public string Street { get; set; }
public string City { get; set; }
public List<int> Scores;
}
class Teacher
{
public string First { get; set; }
public string Last { get; set; }
public int ID { get; set; }
public string City { get; set; }
}
2. Para crear elementos que contengan más de una propiedad del elemento de origen, se puede usar un
inicializador de objeto con un objeto con nombre o un tipo anónimo. En el ejemplo siguiente se muestra
el uso de un tipo anónimo para encapsular dos propiedades de cada elemento Customer :
Para obtener más información, vea Inicializadores de objeto y de colección y Tipos anónimos.
class XMLTransform
{
static void Main()
{
// Create the data source by using a collection initializer.
// The Student class was defined previously in this topic.
List<Student> students = new List<Student>()
{
new Student {First="Svetlana", Last="Omelchenko", ID=111, Scores = new List<int>{97, 92, 81,
60}},
new Student {First="Claire", Last="O’Donnell", ID=112, Scores = new List<int>{75, 84, 91, 39}},
new Student {First="Sven", Last="Mortensen", ID=113, Scores = new List<int>{88, 94, 65, 91}},
};
Para obtener más información, vea Creating XML Trees (C#) (Creación de árboles XML [C#]).
NOTE
No se admite llamar a métodos en expresiones de consulta si la consulta se va a convertir a otro dominio. Por ejemplo, no
se puede llamar a un método normal de C# en LINQ to SQL porque SQL Server no tiene contexto para él. En cambio, se
pueden asignar procedimientos almacenados a los métodos y llamar a los primeros. Para obtener más información, vea
Procedimientos almacenados.
class FormatQuery
{
static void Main()
{
// Data source.
double[] radii = { 1, 2, 3 };
/*
// LINQ query using query syntax.
IEnumerable<string> output =
from rad in radii
select $"Area for a circle with a radius of '{rad}' = {rad * rad * Math.PI:F2}";
*/
Vea también
Language Integrated Query (LINQ) (C#)
LINQ to SQL
LINQ to DataSet
LINQ to XML (C#)
Expresiones de consulta LINQ
select (cláusula)
Relaciones entre tipos en operaciones de consulta
LINQ (C#)
16/09/2021 • 3 minutes to read
Para escribir las consultas eficazmente, es necesario comprender cómo los tipos de las variables en una
operación de consulta completa se relacionan entre sí. Si entiende estas relaciones comprenderá más fácilmente
los ejemplos de LINQ y los ejemplos de código de la documentación. Además, entenderá lo que sucede en
segundo plano cuando los tipos de las variables se declaran implícitamente mediante var .
las operaciones de consulta LINQ tienen un establecimiento fuertemente tipado en el origen de datos, en la
propia consulta y en la ejecución de la consulta. El tipo de las variables de la consulta debe ser compatible con el
tipo de los elementos del origen de datos y con el tipo de la variable de iteración de la instrucción foreach . Este
establecimiento inflexible de tipos garantiza que los errores de tipos se detectan en tiempo de compilación,
cuando aún se pueden corregir antes de que los usuarios los detecten.
Para mostrar estas relaciones de tipos, en la mayoría de los ejemplos siguientes se usan tipos explícitos para
todas las variables. En el último ejemplo se muestra cómo se aplican los mismos principios incluso al usar tipos
implícitos mediante var.
3. Dado que custNameQuery es una secuencia de cadenas, la variable de iteración del bucle foreach
también debe ser string .
En la ilustración siguiente se muestra una transformación un poco más compleja. La instrucción select
devuelve un tipo anónimo que captura solo dos miembros del objeto Customer original.
1. El argumento de tipo del origen de datos siempre es el tipo de la variable de rango de la consulta.
2. Dado que la instrucción select genera un tipo anónimo, la variable de consulta debe declararse
implícitamente mediante var .
3. Dado que el tipo de la variable de consulta es implícito, la variable de iteración del bucle foreach
también debe ser implícita.
La mayoría de las consultas de la documentación introductoria de Language Integrated Query (LINK) se escribe
con la sintaxis de consulta declarativa de LINQ. Pero la sintaxis de consulta debe traducirse en llamadas de
método para .NET Common Language Runtime (CLR) al compilar el código. Estas llamadas de método invocan
los operadores de consulta estándar, que tienen nombres tales como Where , Select , GroupBy , Join , Max y
Average . Puede llamarlas directamente con la sintaxis de método en lugar de la sintaxis de consulta.
La sintaxis de consulta y la sintaxis de método son idénticas desde el punto de vista semántico, pero muchos
usuarios creen que la sintaxis de consulta es mucho más sencilla y fácil de leer. Algunos métodos deben
expresarse como llamadas de método. Por ejemplo, debe usar una llamada de método para expresar una
consulta que recupera el número de elementos que cumplen una condición especificada. También debe usar una
llamada de método para una consulta que recupera el elemento que tiene el valor máximo de una secuencia de
origen. La documentación de referencia de los operadores de consulta estándar del espacio de nombres
System.Linq generalmente usa la sintaxis de método. Por consiguiente, incluso cuando empiece a escribir
consultas de LINK, resulta útil estar familiarizado con el uso de la sintaxis de método en las consultas y en las
propias expresiones de consulta.
//Query syntax:
IEnumerable<int> numQuery1 =
from num in numbers
where num % 2 == 0
orderby num
select num;
//Method syntax:
IEnumerable<int> numQuery2 = numbers.Where(num => num % 2 == 0).OrderBy(n => n);
El resultado de los dos ejemplos es idéntico. Como puede ver, el tipo de variable de consulta es el mismo en
ambos formularios: IEnumerable<T>.
Para entender la consulta basada en métodos, vamos a examinarla más detenidamente. En el lado derecho de la
expresión, observe que la cláusula where ahora se expresa como un método de instancia en el objeto numbers ,
que, como recordará, tiene un tipo de IEnumerable<int> . Si está familiarizado con la interfaz genérica
IEnumerable<T>, sabe que no tiene un método Where . Pero si se invoca la lista de finalización de IntelliSense en
el IDE de Visual Studio, verá no solo un método Where , sino muchos otros métodos tales como Select ,
SelectMany , Join y Orderby . Estos son todos los operadores de consulta estándar.
Aunque parece que IEnumerable<T> se haya redefinido para incluir estos métodos adicionales, en realidad no
es el caso. Los operadores de consulta estándar se implementan como un nuevo tipo de método denominado
método de extensión. Los métodos de extensión "extienden" un tipo existente; se pueden llamar como si fueran
métodos de instancia en el tipo. Los operadores de consulta estándar extienden IEnumerable<T> y esta es la
razón por la que la puede escribir numbers.Where(...) .
Para empezar a usar LINK, lo único que realmente debe saber sobre los métodos de extensión es cómo incluirlos
en el ámbito de la aplicación mediante el uso correcto de directivas using . Desde el punto de vista de la
aplicación, un método de extensión y un método de instancia normal son iguales.
Para obtener más información sobre los métodos de extensión, vea Métodos de extensión. Para obtener más
información sobre los operadores de consulta estándar, vea Información general sobre operadores de consulta
estándar (C#). Algunos proveedores LINK, como LINQ to SQL y LINQ to XML, implementan sus propios
operadores de consulta estándar y otros métodos de extensión para otros tipos además de IEnumerable<T>.
Expresiones lambda
En el ejemplo anterior, observe que la expresión condicional ( num % 2 == 0 ) se pasa como argumento insertado
al método Where : Where(num => num % 2 == 0). Esta expresión insertada se denomina expresión lambda. Se
trata de una forma cómoda de escribir código que, de lo contrario, tendría que escribirse de forma más
compleja como un método anónimo, un delegado genérico o un árbol de expresión. En C#, => es el operador
lambda, que se lee como "va a". La num situada a la izquierda del operador es la variable de entrada que
corresponde a num en la expresión de consulta. El compilador puede deducir el tipo de num porque sabe que
numbers es un tipo IEnumerable<T> genérico. El cuerpo de la expresión lambda es exactamente igual que la
expresión de la sintaxis de consulta o de cualquier otra expresión o instrucción de C#; puede incluir llamadas de
método y otra lógica compleja. El "valor devuelto" es simplemente el resultado de la expresión.
Para empezar a usar LINK, no es necesario emplear muchas expresiones lambda. Pero determinadas consultas
solo se pueden expresar en sintaxis de método y algunas requieren expresiones lambda. Cuando esté más
familiarizado con las expresiones lambda, verá que se trata de una herramienta eficaz y flexible del cuadro de
herramientas de LINK. Para obtener más información, vea Expresiones lambda.
La siguiente sección presenta las nuevas construcciones de lenguaje incluidas en C# 3.0. Aunque estas nuevas
características se usan hasta cierto punto con consultas LINQ, no se limitan a LINQ y se pueden usar en
cualquier contexto en las que se consideren de utilidad.
Expresiones de consulta
Las expresiones de consulta usan una sintaxis declarativa similar a SQL o XQuery para consultar colecciones de
IEnumerable. En tiempo de compilación, la sintaxis de consulta se convierte en llamadas de método a la
implementación de un proveedor de LINQ de los métodos de extensión de operador de consulta estándar. Las
aplicaciones controlan los operadores de consulta estándar que están en el ámbito al especificar el espacio de
nombres adecuado con una directiva using . La siguiente expresión de consulta toma una matriz de cadenas, las
agrupa por el primer carácter de la cadena y ordena los grupos.
var number = 5;
var name = "Virginia";
var query = from str in stringArray
where str[0] == 'm'
select str;
Las variables declaradas como var son tan fuertemente tipadas como las variables cuyo tipo se especifica
explícitamente. El uso de var hace posible crear tipos anónimos, pero solo se puede usar para variables locales.
También se pueden declarar matrices con asignación implícita de tipos.
Para más información, vea Variables locales con asignación implícita de tipos.
Continuando con nuestra clase Customer , suponga que hay un origen de datos denominado IncomingOrders y
que para cada pedido con un OrderSize grande, nos gustaría crear un nuevo Customer basado en ese orden. Se
pueden ejecutar una consulta LINQ en este origen de datos y usar la inicialización de objetos para rellenar una
colección:
El origen de datos puede tener más propiedades ocultas en el montón que la clase Customer , como OrderSize ,
pero con la inicialización de objetos, los datos devueltos por la consulta se moldean en el tipo de datos deseado;
elegimos los datos que son relevantes para nuestra clase. Como resultado, ahora tenemos un IEnumerable
relleno con el nuevo Customer que queríamos. Lo anterior también se puede escribir en la sintaxis de método
de LINQ:
var newLargeOrderCustomers = IncomingOrders.Where(x => x.OrderSize > 5).Select(y => new Customer { Name =
y.Name, Phone = y.Phone });
Tipos anónimos
Un tipo anónimo se construye por el compilador y el nombre del tipo solo está disponible para el compilador.
Los tipos anónimos son una manera cómoda de agrupar un conjunto de propiedades temporalmente en un
resultado de consulta sin tener que definir un tipo con nombre independiente. Los tipos anónimos se inicializan
con una nueva expresión y un inicializador de objeto, como se muestra aquí:
Métodos de extensión.
Un método de extensión es un método estático que se puede asociar con un tipo, por lo que puede llamarse
como si fuera un método de instancia en el tipo. Esta característica permite, en efecto, "agregar" nuevos
métodos a los tipos existentes sin tener que modificarlos realmente. Los operadores de consulta estándar son
un conjunto de métodos de extensión que proporcionan funciones de consultas LINQ para cualquier tipo que
implemente IEnumerable<T>.
Para obtener más información, vea Métodos de extensión.
Expresiones lambda
Una expresión lambda es una función insertada que usa el operador => para separar los parámetros de entrada
del cuerpo de la función y que se puede convertir en tiempo de compilación en un delegado o un árbol de
expresión. En la programación de LINQ, encontrará expresiones lambda al realizar llamadas de método directas
a los operadores de consulta estándar.
Para obtener más información, consulte:
Funciones anónimas
Expresiones lambda
Árboles de expresión (C#)
Vea también
Language Integrated Query (LINQ) (C#)
Tutorial: Escribir consultas en C# (LINQ)
16/09/2021 • 10 minutes to read
Este tutorial muestra las características del lenguaje C# que se usan para escribir expresiones de consulta de
LINQ.
Crear un proyecto de C#
NOTE
Las siguientes instrucciones se aplican a Visual Studio. Si usa otro entorno de desarrollo, cree un proyecto de consola con
una referencia a System.Core.dll y una directiva using para el espacio de nombres System.Linq.
Crear la consulta
Para crear una consulta simple
En el método Main de la aplicación, cree una consulta simple que, cuando se ejecute, genere una lista de
todos los alumnos cuya puntuación en la primera prueba haya sido superior a 90. Tenga en cuenta que,
dado que se ha seleccionado todo el objeto Student , el tipo de la consulta es IEnumerable<Student> .
Aunque el código podría usar tipos implícitos con la palabra clave var, se usan tipos explícitos para
mostrar claramente los resultados. (Para obtener más información sobre var , vea Implicitly Typed Local
Variables [Variables locales con tipo implícito]).
Tenga también en cuenta que la variable de rango de la consulta, student , actúa como referencia a cada
Student del origen, lo que proporciona acceso a miembro para cada objeto.
Ejecutar la consulta
Para ejecutar la consulta
1. Escriba ahora el bucle foreach que hará que la consulta se ejecute. Tenga en cuenta los siguiente sobre el
código:
A cada elemento de la secuencia devuelta se accede mediante la variable de iteración del bucle
foreach .
El tipo de esta variables es Student y el tipo de la variable de consulta es compatible,
IEnumerable<Student> .
2. Tras agregar este código, compile y ejecute la aplicación para ver los resultados en la ventana Consola .
// Output:
// Omelchenko, Svetlana
// Garcia, Cesar
// Fakhouri, Fadi
// Feng, Hanying
// Garcia, Hugo
// Adams, Terry
// Zabokritski, Eugene
// Tucker, Michael
Modificar la consulta
Para ordenar los resultados
1. Le resultará más fácil examinar los resultados si se muestran con algún tipo de orden. Puede ordenar la
secuencia devuelta por cualquier campo accesible de los elementos de origen. Por ejemplo, la cláusula
orderby siguiente ordena los resultados alfabéticamente de la A a la Z por el apellido de cada alumno.
Agregue la cláusula orderby siguiente a la consulta, inmediatamente después de la instrucción where y
antes de la instrucción select :
2. Cambie ahora la cláusula orderby para que ordene los resultados en orden inverso según la puntuación
en la primera prueba, de la puntuación más alta a la más baja.
2. Tenga en cuenta que el tipo de la consulta ha cambiado. Ahora genera una secuencia de grupos que
tienen un tipo char como clave y una secuencia de objetos Student . Dado que el tipo de la consulta ha
cambiado, el siguiente código cambia también el bucle de ejecución foreach :
// Output:
// O
// Omelchenko, Svetlana
// O'Donnell, Claire
// M
// Mortensen, Sven
// G
// Garcia, Cesar
// Garcia, Debra
// Garcia, Hugo
// F
// Fakhouri, Fadi
// Feng, Hanying
// T
// Tucker, Lance
// Tucker, Michael
// A
// Adams, Terry
// Z
// Zabokritski, Eugene
var studentQuery3 =
from student in students
group student by student.Last[0];
// Output:
// O
// Omelchenko, Svetlana
// O'Donnell, Claire
// M
// Mortensen, Sven
// G
// Garcia, Cesar
// Garcia, Debra
// Garcia, Hugo
// F
// Fakhouri, Fadi
// Feng, Hanying
// T
// Tucker, Lance
// Tucker, Michael
// A
// Adams, Terry
// Z
// Zabokritski, Eugene
Para obtener más información sobre var, vea Variables locales con asignación implícita de tipos.
Para ordenar los grupos por su valor clave
1. Al ejecutar la consulta anterior, observará que los grupos no están en orden alfabético. Para cambiar esto,
debe especificar una cláusula orderby después de la cláusula group . Pero para usar una cláusula
orderby , necesita primero un identificador que actúe como referencia a los grupos creados por la
cláusula group . El identificador se especifica con la palabra clave into , de la manera siguiente:
var studentQuery4 =
from student in students
group student by student.Last[0] into studentGroup
orderby studentGroup.Key
select studentGroup;
// Output:
//A
// Adams, Terry
//F
// Fakhouri, Fadi
// Feng, Hanying
//G
// Garcia, Cesar
// Garcia, Debra
// Garcia, Hugo
//M
// Mortensen, Sven
//O
// Omelchenko, Svetlana
// O'Donnell, Claire
//T
// Tucker, Lance
// Tucker, Michael
//Z
// Zabokritski, Eugene
Cuando ejecute esta consulta, verá que los grupos aparecen ahora ordenados alfabéticamente.
Para incluir un identificador mediante let
1. Puede usar la palabra clave let para incluir un identificador con cualquier resultado de la expresión en
la expresión de consulta. Este identificador puede resultar cómodo, como en el ejemplo siguiente, o
mejorar el rendimiento almacenando los resultados de una expresión para que no tenga que calcularse
varias veces.
// studentQuery5 is an IEnumerable<string>
// This query returns those students whose
// first test score was higher than their
// average score.
var studentQuery5 =
from student in students
let totalScore = student.Scores[0] + student.Scores[1] +
student.Scores[2] + student.Scores[3]
where totalScore / 4 < student.Scores[0]
select student.Last + " " + student.First;
// Output:
// Omelchenko Svetlana
// O'Donnell Claire
// Mortensen Sven
// Garcia Cesar
// Fakhouri Fadi
// Feng Hanying
// Garcia Hugo
// Adams Terry
// Zabokritski Eugene
// Tucker Michael
var studentQuery6 =
from student in students
let totalScore = student.Scores[0] + student.Scores[1] +
student.Scores[2] + student.Scores[3]
select totalScore;
// Output:
// Class average score = 334.166666666667
// Output:
// The Garcias in the class are:
// Cesar
// Debra
// Hugo
2. El código anterior en este tutorial indicaba que la puntuación media de la clase es aproximadamente 334.
Para generar una secuencia de Students cuya puntuación total sea superior al promedio de la clase,
junto con su Student ID , puede usar un tipo anónimo en la instrucción select :
var studentQuery8 =
from student in students
let x = student.Scores[0] + student.Scores[1] +
student.Scores[2] + student.Scores[3]
where x > averageScore
select new { id = student.ID, score = x };
// Output:
// Student ID: 113, Score: 338
// Student ID: 114, Score: 353
// Student ID: 116, Score: 369
// Student ID: 117, Score: 352
// Student ID: 118, Score: 343
// Student ID: 120, Score: 341
// Student ID: 122, Score: 368
Pasos siguientes
Cuando se haya familiarizado con los aspectos básicos del uso de consultas en C#, estará preparado para leer la
documentación y ejemplos del tipo específico de proveedor LINQ que le interese:
LINQ to SQL
LINQ to DataSet
LINQ to XML (C#)
LINQ to Objects (C#)
Vea también
Language Integrated Query (LINQ) (C#)
Expresiones de consulta LINQ
Información general sobre operadores de consulta
estándar (C#)
16/09/2021 • 4 minutes to read
Los operadores de consulta estándar son los métodos que constituyen el modelo LINQ. La mayoría de estos
métodos funciona en secuencias; donde una secuencia es un objeto cuyo tipo implementa la interfaz
IEnumerable<T> o la interfaz IQueryable<T>. Los operadores de consulta estándar ofrecen funcionalidades de
consulta, como las funciones de filtrado, proyección, agregación y ordenación, entre otras.
Hay dos conjuntos de operadores de consulta estándar de LINQ: uno que actúa sobre objetos de tipo
IEnumerable<T> y otro sobre objetos de tipo IQueryable<T>. Los métodos que forman cada conjunto son
miembros estáticos de las clases Enumerable y Queryable, respectivamente. Se definen como métodos de
extensión del tipo en el que actúan. Se puede llamar a los métodos de extensión mediante sintaxis de método
estático o sintaxis de método de instancia.
Además, varios métodos de operador de consulta estándar funcionan en tipos distintos de los que se basan en
IEnumerable<T> o IQueryable<T>. El tipo Enumerable define dos métodos que funcionan en objetos de tipo
IEnumerable. Estos métodos, Cast<TResult>(IEnumerable) y OfType<TResult>(IEnumerable), le permiten
habilitar una colección no genérica o no parametrizada para consultarse en el patrón LINQ. Para ello, se crea una
colección de objetos fuertemente tipados. La clase Queryable define dos métodos similares, Cast<TResult>
(IQueryable) y OfType<TResult>(IQueryable), que funcionan en objetos de tipo IQueryable.
Los operadores de consulta estándar difieren en sus intervalos de ejecución, dependiendo de que devuelvan un
valor singleton o una secuencia de valores. Esos métodos que devuelven un valor singleton (por ejemplo,
Average y Sum) se ejecutan inmediatamente. Los métodos que devuelven una secuencia aplazan la ejecución de
la consulta y devuelven un objeto enumerable.
En el caso de los métodos que actúan en colecciones en memoria, es decir, los métodos que extienden
IEnumerable<T>, el objeto enumerable devuelto captura los argumentos que se han pasado al método. Cuando
se enumera ese objeto, se emplea la lógica del operador de consulta y se devuelven los resultados de la
consulta.
En cambio, los métodos que extienden IQueryable<T> no implementan ningún comportamiento de realización
de consultas. Compilan un árbol de expresión que representa la consulta que se va a realizar. El procesamiento
de consultas se controla mediante el objeto IQueryable<T> de origen.
Las llamadas a métodos de consulta se pueden encadenar juntas en una consulta, lo que permite que las
consultas se conviertan en complejas de forma arbitraria.
En el ejemplo de código siguiente se muestra el uso de los operadores de consulta estándar para obtener
información sobre una secuencia.
string sentence = "the quick brown fox jumps over the lazy dog";
// Split the string into individual words to create a collection.
string[] words = sentence.Split(' ');
Secciones relacionadas
Los vínculos siguientes llevan a artículos que ofrecen información adicional sobre los distintos operadores de
consulta estándar según la funcionalidad.
Ordenación de datos [C#]
Operaciones set [C#]
Filtrado de datos [C#]
Operaciones cuantificadoras [C#]
Operaciones de proyección [C#]
Realizar particiones de datos [C#]
Operaciones de combinación [C#]
Agrupar datos [C#]
Operaciones de generación [C#]
Operaciones de igualdad [C#]
Operaciones de elementos [C#]
Convertir tipos de datos [C#]
Operaciones de concatenación [C#]
Operaciones de agregación [C#]
Vea también
Enumerable
Queryable
Introducción a las consultas LINQ (C#)
Sintaxis de las expresiones de consulta para operadores de consulta estándar (C#)
Clasificación de operadores de consulta estándar por modo de ejecución (C#)
Métodos de extensión
Sintaxis de las expresiones de consulta para
operadores de consulta estándar (C#)
16/09/2021 • 2 minutes to read
Algunos de los operadores de consulta estándar que se usan con más frecuencia tienen una sintaxis especial de
palabras clave de lenguaje C# para que se puedan invocar como parte de una expresión de consulta. Una
expresión de consulta constituye una forma diferente de expresar una consulta, más legible que su equivalente
basada en métodos. Las cláusulas de las expresiones de consulta se convierten en llamadas a los métodos de
consulta en tiempo de compilación.
Cast Use una variable de rango con tipo explícito, por ejemplo:
GroupBy group … by
o bien
group … by … into …
OrderBy<TSource,TKey>(IEnumerable<TSource>, orderby
Func<TSource,TKey>)
(Para obtener más información, vea orderby (Cláusula)).
Select select
ThenBy<TSource,TKey>(IOrderedEnumerable<TSource>, orderby …, …
Func<TSource,TKey>)
(Para obtener más información, vea orderby (Cláusula)).
Where where
Consulte también
Enumerable
Queryable
Información general sobre operadores de consulta estándar (C#)
Clasificación de operadores de consulta estándar por modo de ejecución (C#)
Clasificación de operadores de consulta estándar
por modo de ejecución (C#)
16/09/2021 • 3 minutes to read
Las implementaciones de LINQ to Objects de los métodos de operador de consulta estándar se ejecutan de una
de dos formas principales: inmediata o aplazada. Los operadores de consulta que usan la ejecución aplazada
pueden dividirse además en dos categorías: de streaming y de no streaming. Si sabe cómo se ejecutan los
diferentes operadores de consulta, puede servirle para entender los resultados que se obtienen de una consulta
determinada. Esto es especialmente cierto si se está cambiando el origen de datos o si se está creando una
consulta sobre otra. En este tema se clasifican los operadores de consulta estándar según su modo de ejecución.
Modos de ejecución
Inmediato
La ejecución inmediata significa que se lee el origen de datos y la operación se realiza en el punto en el código
donde se declara la consulta. Todos los operadores de consulta estándar que devuelven un resultado único no
enumerable se ejecutan de manera inmediata.
Aplazada
La ejecución aplazada significa que la operación no se realiza en el punto en el código donde se declara la
consulta. La operación se realiza solo cuando se enumera la variable de consulta, por ejemplo, mediante una
instrucción foreach . Esto significa que los resultados de ejecutar la consulta dependen del contenido del origen
de datos cuando se ejecuta la consulta en lugar de cuando se define la consulta. Si la variable de consulta se
enumera varias veces, es posible que los resultados difieran cada vez. Casi todos los operadores de consulta
estándar cuyo tipo de valor devuelto es IEnumerable<T> o IOrderedEnumerable<TElement> se ejecutan de una
manera diferida.
Los operadores de consulta que usan la ejecución aplazada pueden clasificarse además como de streaming o de
no streaming.
Streaming
Los operadores de streaming no deben leer todos los datos de origen antes de que produzcan elementos. En el
momento de la ejecución, un operador de streaming realiza su operación en cada elemento de origen mientras
se lee y proporciona el elemento si es necesario. Un operador de streaming continúa leyendo los elementos de
origen hasta que se puede generar un elemento de resultado. Esto significa que es posible leer más de un
elemento de origen para generar un elemento de resultado.
No streaming
Los operadores de no streaming deben leer todos los datos de origen antes de poder proporcionar un elemento
de resultado. Las operaciones como la ordenación o la agrupación pertenecen a esta categoría. En tiempo de
ejecución, los operadores de consulta de no streaming leen todos los datos de origen, los colocan en una
estructura de datos, realizan la operación y proporcionan los elementos resultantes.
Tabla de clasificación
En la tabla siguiente se clasifica cada método de operador de consulta estándar según su método de ejecución.
NOTE
Si un operador se marca en dos columnas, dos secuencias de entrada intervienen en la operación, y cada secuencia se
evalúa de manera diferente. En estos casos, siempre es la primera secuencia de la lista de parámetros la que se evalúa en
un modo de transmisión diferido.
Aggregate TSource x
All Boolean x
Any Boolean x
AsEnumerable IEnumerable<T> X
Cast IEnumerable<T> x
Concat IEnumerable<T> x
Contains Boolean x
Count Int32 x
DefaultIfEmpty IEnumerable<T> x
Distinct IEnumerable<T> X
ElementAt TSource X
ElementAtOrDefault TSource x
Empty IEnumerable<T> x
Except IEnumerable<T> x X
First TSource X
FirstOrDefault TSource x
GroupBy IEnumerable<T> x
GroupJoin IEnumerable<T> x x
Intersect IEnumerable<T> x x
Join IEnumerable<T> x X
O P ERA DO R DE E JEC UC IÓ N E JEC UC IÓ N
C O N SULTA E JEC UC IÓ N A P L A Z A DA DE A P L A Z A DA DE N O
ESTÁ N DA R T IP O DEVUELTO IN M EDIATA ST REA M IN G ST REA M IN G
Last TSource X
LastOrDefault TSource x
LongCount Int64 X
OfType IEnumerable<T> x
OrderBy IOrderedEnumerable x
<TElement>
OrderByDescending IOrderedEnumerable x
<TElement>
Range IEnumerable<T> x
Repeat IEnumerable<T> x
Reverse IEnumerable<T> x
Select IEnumerable<T> x
SelectMany IEnumerable<T> x
SequenceEqual Boolean X
Single TSource X
SingleOrDefault TSource x
Skip IEnumerable<T> x
SkipWhile IEnumerable<T> X
Take IEnumerable<T> x
TakeWhile IEnumerable<T> x
ThenBy IOrderedEnumerable x
<TElement>
O P ERA DO R DE E JEC UC IÓ N E JEC UC IÓ N
C O N SULTA E JEC UC IÓ N A P L A Z A DA DE A P L A Z A DA DE N O
ESTÁ N DA R T IP O DEVUELTO IN M EDIATA ST REA M IN G ST REA M IN G
ThenByDescending IOrderedEnumerable X
<TElement>
ToDictionary Dictionary<TKey,TVal x
ue>
ToList IList<T> x
ToLookup ILookup<TKey,TElem x
ent>
Union IEnumerable<T> x
Where IEnumerable<T> X
Consulte también
Enumerable
Información general sobre operadores de consulta estándar (C#)
Sintaxis de las expresiones de consulta para operadores de consulta estándar (C#)
LINQ to Objects (C#)
Ordenación de datos (C#)
16/09/2021 • 2 minutes to read
Una operación de ordenación ordena los elementos de una secuencia según uno o varios atributos. El primer
criterio de ordenación realiza una ordenación primaria de los elementos. Al especificar un segundo criterio de
ordenación, se pueden ordenar los elementos dentro de cada grupo de ordenación primaria.
En la ilustración siguiente se muestran los resultados de una operación de ordenación alfabética en una
secuencia de caracteres:
Los métodos de operador de consulta estándar que ordenan datos de datos se enumeran en la sección
siguiente.
Métodos
SIN TA XIS DE L A EXP RESIÓ N
N O M B RE DEL M ÉTO DO DESC RIP C IÓ N DE C O N SULTA DE C # M Á S IN F O RM A C IÓ N
Queryable.OrderByDescendi
ng
the
fox
quick
brown
jumps
*/
the
quick
jumps
fox
brown
*/
fox
the
brown
jumps
quick
*/
the
fox
quick
jumps
brown
*/
Consulte también
System.Linq
Información general sobre operadores de consulta estándar (C#)
orderby (cláusula)
Ordenar los resultados de una cláusula join
Procedimiento para ordenar o filtrar datos de texto por palabra o campo (LINQ) (C#)
Operaciones set (C#)
16/09/2021 • 2 minutes to read
Las operaciones set de LINQ se refieren a operaciones de consulta que generan un conjunto de resultados en
función de la presencia o ausencia de elementos equivalentes dentro de la misma colección o en distintas
colecciones (o conjuntos).
Los métodos del operador de consulta estándar que realizan operaciones set se indican en la sección siguiente.
Métodos
SIN TA XIS DE L A EXP RESIÓ N
N O M B RE DEL M ÉTO DO DESC RIP C IÓ N DE C O N SULTA DE C # M Á S IN F O RM A C IÓ N
Except
En el ejemplo siguiente se muestra el comportamiento de Enumerable.Except. La secuencia devuelta solo
contiene los elementos de la primera secuencia de entrada que no están en la segunda secuencia de entrada.
Formar intersección
En el ejemplo siguiente se muestra el comportamiento de Enumerable.Intersect. La secuencia devuelta contiene
los elementos que son comunes a las dos secuencias de entrada.
string[] planets1 = { "Mercury", "Venus", "Earth", "Jupiter" };
string[] planets2 = { "Mercury", "Earth", "Mars", "Jupiter" };
Unión
En el siguiente ejemplo se muestra una operación de unión en dos secuencias de caracteres. La secuencia
devuelta contiene los elementos únicos de las dos secuencias de entrada.
Consulte también
System.Linq
Información general sobre operadores de consulta estándar (C#)
Procedimiento para combinar y comparar colecciones de cadenas (LINQ) (C#)
Procedimiento para buscar la diferencia de conjuntos entre dos listas (LINQ) (C#)
Filtrado de datos (C#)
16/09/2021 • 2 minutes to read
El filtrado hace referencia a la operación de restringir el conjunto de resultados, de manera que solo contenga
los elementos que cumplen una condición especificada. También se conoce como selección.
En la ilustración siguiente se muestran los resultados de filtrar una secuencia de caracteres. El predicado de la
operación de filtrado especifica que el carácter debe ser "A".
Los métodos del operador de consulta estándar que realizan selecciones se indican en la sección siguiente.
Métodos
SIN TA XIS DE L A EXP RESIÓ N
N O M B RE DEL M ÉTO DO DESC RIP C IÓ N DE C O N SULTA DE C # M Á S IN F O RM A C IÓ N
the
fox
*/
Consulte también
System.Linq
Información general sobre operadores de consulta estándar (C#)
where (cláusula)
Especificar dinámicamente filtros con predicado en tiempo de ejecución
Procedimiento para consultar los metadatos de un ensamblado con reflexión (LINQ) (C#)
Procedimiento para buscar archivos con un nombre o atributo especificados (C#)
Procedimiento para ordenar o filtrar datos de texto por palabra o campo (LINQ) (C#)
Operaciones cuantificadoras (C#)
16/09/2021 • 3 minutes to read
Las operaciones cuantificadoras devuelven un valor Boolean que indica si algunos o todos los elementos de una
secuencia cumplen una condición.
En la siguiente ilustración se muestran dos operaciones cuantificadoras diferentes en dos secuencias de origen
distintas. La primera operación pregunta si uno o varios de los elementos son el carácter "A", y el resultado es
true . La segunda operación pregunta si todos los elementos son el carácter "A", y el resultado es true .
Los métodos del operador de consulta estándar que realizan operaciones cuantificadoras se indican en la
sección siguiente.
Métodos
SIN TA XIS DE L A EXP RESIÓ N
N O M B RE DEL M ÉTO DO DESC RIP C IÓ N DE C O N SULTA DE C # M Á S IN F O RM A C IÓ N
Cualquiera
En el ejemplo siguiente se usa Any para comprobar que las cadenas se inician con "o".
class Market
{
public string Name { get; set; }
public string[] Items { get; set; }
}
// Determine which market have any fruit names start with 'o'
IEnumerable<string> names = from market in markets
where market.Items.Any(item => item.StartsWith("o"))
select market.Name;
class Market
{
public string Name { get; set; }
public string[] Items { get; set; }
}
Consulte también
System.Linq
Información general sobre operadores de consulta estándar (C#)
Especificar dinámicamente filtros con predicado en tiempo de ejecución
Procedimiento para buscar frases que contengan un conjunto especificado de palabras (LINQ) (C#)
Operaciones de proyección (C#)
16/09/2021 • 3 minutes to read
El término "proyección" hace referencia a la operación de transformar un objeto en una nueva forma que, a
menudo, consta solo de aquellas propiedades que se usarán posteriormente. Utilizando la proyección, puede
construir un tipo nuevo creado a partir de cada objeto. Se puede proyectar una propiedad y realizar una función
matemática en ella. También puede proyectar el objeto original sin cambiarlo.
Los métodos del operador de consulta estándar que realizan proyecciones se indican en la sección siguiente.
Métodos
SIN TA XIS DE L A EXP RESIÓ N
N O M B RE DEL M ÉTO DO DESC RIP C IÓ N DE C O N SULTA DE C # M Á S IN F O RM A C IÓ N
a
a
a
d
*/
SelectMany
En el ejemplo siguiente se usan varias cláusulas from para proyectar cada palabra de todas las cadenas de una
lista de cadenas.
List<string> phrases = new List<string>() { "an apple a day", "the quick brown fox" };
an
apple
a
day
the
quick
brown
fox
*/
En esta ilustración se muestra cómo SelectMany() concatena la secuencia intermedia de matrices en un valor de
resultado final que contiene cada uno de los valores de todas las matrices intermedias.
Ejemplo de código
En el ejemplo siguiente se compara el comportamiento de Select() y SelectMany() . El código crea un "ramo"
de flores tomando los dos primeros elementos de cada lista de nombres de flores de la colección de origen. En
este ejemplo, el "valor único" que la función de transformación Select<TSource,TResult>
(IEnumerable<TSource>, Func<TSource,TResult>) usa es una colección de valores. Para ello, se requiere el bucle
adicional foreach a fin de enumerar cada una de las cadenas de cada subsecuencia.
class Bouquet
{
public List<string> Flowers { get; set; }
}
Consulte también
System.Linq
Información general sobre operadores de consulta estándar (C#)
select (cláusula)
Procedimiento para rellenar colecciones de objetos de varios orígenes (LINQ) (C#)
Procedimiento para dividir un archivo en varios mediante el uso de grupos (LINQ) (C#)
Realizar particiones de datos (C#)
16/09/2021 • 2 minutes to read
Partición en LINQ es la operación de dividir una secuencia de entrada en dos secciones, sin reorganizar los
elementos, y devolver después una de las secciones.
En la siguiente ilustración se muestran los resultados de tres operaciones de partición diferentes en una
secuencia de caracteres. La primera operación devuelve los tres primeros elementos de la secuencia. La segunda
operación omite los tres primeros elementos y devuelve los restantes. La tercera operación omite los dos
primeros elementos de la secuencia y devuelve los tres siguientes.
Los métodos de operador de consulta estándar que realizan particiones de las secuencias se enumeran en la
sección siguiente.
Operadores
SIN TA XIS DE L A EXP RESIÓ N
N O M B RE DE O P ERA DO R DESC RIP C IÓ N DE C O N SULTA DE C # M Á S IN F O RM A C IÓ N
Consulte también
System.Linq
Información general sobre operadores de consulta estándar (C#)
Operaciones Join (C#)
16/09/2021 • 4 minutes to read
Una combinación de dos orígenes de datos es la asociación de objetos de un origen de datos con los objetos
que comparten un atributo común en otro origen de datos.
La combinación Join es una operación importante en las consultas destinadas a orígenes de datos cuyas
relaciones entre sí no se pueden seguir directamente. En la programación orientada a objetos, esto podría
significar una correlación entre objetos que no está modelada, como el sentido contrario de una relación
unidireccional. Un ejemplo de una relación unidireccional es una clase Cliente que tiene una propiedad de tipo
Ciudad, pero la clase Ciudad no tiene una propiedad que sea una colección de objetos Cliente. Si tiene una lista
de objetos Ciudad y quiere encontrar todos los clientes en cada ciudad, podría usar una operación de
combinación para encontrarlos.
Los métodos de combinación que se han proporcionado en el marco de LINQ son Join y GroupJoin. Estos
métodos efectúan combinaciones de igualdad, o combinaciones que hacen corresponder dos orígenes de datos
en función de la igualdad de sus claves. (Para comparar, Transact-SQL admite otros operadores de combinación
aparte de 'igual', por ejemplo, 'menor que'). En términos de base de datos relacional, Join implementa una
combinación interna, un tipo de combinación en la que solo se devuelven los objetos que tienen una
correspondencia en el otro conjunto de datos. El método GroupJoin no tiene equivalente directo en términos de
bases de datos relacionales; pero implementa un superconjunto de combinaciones internas y combinaciones
externas izquierdas. Una combinación externa izquierda es una combinación que devuelve cada elemento del
primer origen de datos (izquierda), aunque no tenga elementos correlacionados en el otro origen de datos.
En la ilustración siguiente se muestra una vista conceptual de dos conjuntos y los elementos de esos conjuntos
que se incluyen en una combinación interna o en una combinación externa izquierda.
Métodos
SIN TA XIS DE L A EXP RESIÓ N
N O M B RE DEL M ÉTO DO DESC RIP C IÓ N DE C O N SULTA DE C # M Á S IN F O RM A C IÓ N
class Category
{
public int Id { get; set; }
public string CategoryName { get; set; }
}
GroupJoin
En el ejemplo siguiente se usa la cláusula join … in … on … equals … into … para combinar dos secuencias en
función de un valor específico y se agrupan las coincidencias resultantes de cada elemento:
class Product
{
public string Name { get; set; }
public int CategoryId { get; set; }
}
class Category
{
public int Id { get; set; }
public string CategoryName { get; set; }
}
Consulte también
System.Linq
Información general sobre operadores de consulta estándar (C#)
Tipos anónimos
Formular combinaciones Join y consultas entre productos
join (cláusula)
Join usando claves compuestas
Procedimiento para combinar contenido de archivos no similares (LINQ) (C#)
Ordenar los resultados de una cláusula join
Realizar operaciones de combinación personalizadas
Realizar combinaciones agrupadas
Realizar combinaciones internas
Realizar operaciones de combinación externa izquierda
Procedimiento para rellenar colecciones de objetos de varios orígenes (LINQ) (C#)
Agrupar datos (C#)
16/09/2021 • 2 minutes to read
El agrupamiento hace referencia a la operación de colocar los datos en grupos de manera que los elementos de
cada grupo compartan un atributo común.
La ilustración siguiente muestra los resultados de agrupar una secuencia de caracteres. La clave de cada grupo
es el carácter.
Los métodos de operador de consulta estándar que agrupan elementos de datos se enumeran en la sección
siguiente.
Métodos
SIN TA XIS DE L A EXP RESIÓ N
N O M B RE DEL M ÉTO DO DESC RIP C IÓ N DE C O N SULTA DE C # M Á S IN F O RM A C IÓ N
Odd numbers:
35
3987
199
329
Even numbers:
44
200
84
4
446
208
*/
Consulte también
System.Linq
Información general sobre operadores de consulta estándar (C#)
group (cláusula)
Crear grupos anidados
Procedimiento para agrupar archivos por extensión (LINQ) (C#)
Agrupar los resultados de consultas
Realizar una subconsulta en una operación de agrupación
Procedimiento para dividir un archivo en varios mediante el uso de grupos (LINQ) (C#)
Operaciones de generación (C#)
16/09/2021 • 2 minutes to read
Métodos
SIN TA XIS DE L A EXP RESIÓ N
N O M B RE DEL M ÉTO DO DESC RIP C IÓ N DE C O N SULTA DE C # M Á S IN F O RM A C IÓ N
Consulte también
System.Linq
Información general sobre operadores de consulta estándar (C#)
Operaciones de igualdad (C#)
16/09/2021 • 2 minutes to read
Dos secuencias cuyos respectivos elementos sean iguales y que tengan el mismo número de elementos se
consideran iguales.
Métodos
SIN TA XIS DE L A EXP RESIÓ N
N O M B RE DEL M ÉTO DO DESC RIP C IÓ N DE C O N SULTA DE C # M Á S IN F O RM A C IÓ N
Consulte también
System.Linq
Información general sobre operadores de consulta estándar (C#)
Procedimiento para comparar el contenido de dos carpetas (LINQ) (C#)
Operaciones de elementos (C#)
16/09/2021 • 2 minutes to read
Métodos
SIN TA XIS DE L A EXP RESIÓ N
N O M B RE DEL M ÉTO DO DESC RIP C IÓ N DE C O N SULTA DE C # M Á S IN F O RM A C IÓ N
Consulte también
System.Linq
Información general sobre operadores de consulta estándar (C#)
Procedimiento para buscar el archivo o archivos de mayor tamaño en un árbol de directorios (LINQ) (C#)
Convertir tipos de datos (C#)
16/09/2021 • 2 minutes to read
Métodos
En la siguiente tabla se muestran los métodos de operadores de consulta estándar que efectúan conversiones
de tipo de datos.
Los métodos de conversión de esta tabla cuyos nombres comienzan por "As" cambian el tipo estático de la
colección de origen, pero no lo enumeran. Los métodos cuyos nombres empiezan por "To" enumeran la
colección de origen y colocan los elementos en el tipo de colección correspondiente.
Conversión de tipos Convierte los elementos de Use una variable de rango Enumerable.Cast
explícita una colección en un tipo con tipo explícito. Por
especificado. ejemplo: Queryable.Cast
class Plant
{
public string Name { get; set; }
}
Los métodos del operador de consulta estándar que efectúan una concatenación se indican en la siguiente
sección.
Métodos
SIN TA XIS DE L A EXP RESIÓ N
N O M B RE DEL M ÉTO DO DESC RIP C IÓ N DE C O N SULTA DE C # M Á S IN F O RM A C IÓ N
Consulte también
System.Linq
Información general sobre operadores de consulta estándar (C#)
Procedimiento para combinar y comparar colecciones de cadenas (LINQ) (C#)
Operaciones de agregación (C#)
16/09/2021 • 2 minutes to read
Una operación de agregación calcula un valor único a partir de una colección de valores. Un ejemplo de
operación de agregación es calcular el promedio de temperatura diaria a partir de los valores de temperatura
diaria durante un mes.
En la siguiente ilustración se muestran los resultados de dos operaciones de agregación diferentes en una
secuencia de números. La primera operación suma los números. La segunda operación devuelve el valor
máximo de la secuencia.
Los métodos del operador de consulta estándar que realizan operaciones de agregación se indican en la sección
siguiente.
Métodos
SIN TA XIS DE L A EXP RESIÓ N
N O M B RE DEL M ÉTO DO DESC RIP C IÓ N DE C O N SULTA DE C # M Á S IN F O RM A C IÓ N
Consulte también
System.Linq
Información general sobre operadores de consulta estándar (C#)
Procedimiento para calcular valores de columna en un archivo de texto CSV (LINQ) (C#)
Procedimiento para buscar el archivo o archivos de mayor tamaño en un árbol de directorios (LINQ) (C#)
Procedimiento para buscar el número total de bytes de un conjunto de carpetas (LINQ) (C#)
LINQ to Objects (C#)
16/09/2021 • 2 minutes to read
El término "LINQ to Objects" se refiere al uso de consultas LINQ con cualquier colección IEnumerable o
IEnumerable<T> directamente, sin usar un proveedor o una API de LINQ intermedios como LINQ to SQL o
LINQ to XML. Puede usar LINQ para consultar cualquier colección enumerable, como List<T>, Array o
Dictionary<TKey,TValue>. La colección puede haberla definido el usuario, o bien puede que la haya devuelto
una API de .NET.
Básicamente, LINQ to Objects representa un nuevo enfoque para las colecciones. En el sistema antiguo, tenía
que escribir complejos bucles foreach que especificaban cómo recuperar los datos de una colección. En el
enfoque de LINQ, se escribe código declarativo que describe qué se quiere recuperar.
Además, las consultas LINQ ofrecen tres ventajas principales respecto a los bucles foreach tradicionales:
Son más concisas y legibles, especialmente cuando se filtran varias condiciones.
Proporcionan funcionalidades eficaces para filtrar, ordenar y agrupar con un código de aplicación
mínimo.
Se pueden migrar a otros orígenes de datos con muy poca o ninguna modificación.
Por lo general, cuanto más compleja es la operación que se quiere realizar en los datos, más ventajas se
obtienen al usar LINQ en lugar de las técnicas de iteración tradicionales.
El propósito de esta sección es mostrar el enfoque de LINQ con algunos ejemplos seleccionados. No pretende
ser exhaustiva.
En esta sección
LINQ y cadenas (C#)
Explica cómo se puede usar LINQ para consultar y transformar cadenas y colecciones de cadenas. También
incluye vínculos a artículos que muestran estos principios.
LINQ y reflexión (C#)
Vincula a un ejemplo que muestra cómo usa LINQ la reflexión.
LINQ y directorios de archivos (C#)
Explica cómo se puede usar LINQ para interactuar con sistemas de archivos. También incluye vínculos a artículos
que muestran estos conceptos.
Procedimiento para consultar un objeto ArrayList con LINQ (C#)
Muestra cómo consultar un objeto ArrayList en C#.
Procedimiento para agregar métodos personalizados para las consultas LINQ (C#)
Explica cómo extender el conjunto de métodos que puede usar para consultas LINQ agregando métodos de
extensión a la interfaz IEnumerable<T>.
Language Integrated Query (LINQ) (C#)
Proporciona vínculos a artículos que explican LINQ e incluye ejemplos de código que realizan consultas.
LINQ y cadenas (C#)
16/09/2021 • 2 minutes to read
Se puede usar LINQ para consultar y transformar cadenas y colecciones de cadenas. Puede resultar
especialmente útil con datos semiestructurados de archivos de texto. Las consultas LINQ se pueden combinar
con funciones de cadena tradicionales y expresiones regulares. Por ejemplo, puede usar el método String.Split o
Regex.Split para crear una matriz de cadenas que después puede consultar o modificar mediante LINQ. Puede
usar el método Regex.IsMatch en la cláusula where de una consulta LINQ. Y puede usar LINQ para consultar o
modificar los resultados MatchCollection que se han devuelto mediante una expresión regular.
También puede usar las técnicas descritas en esta sección para transformar datos de texto semiestructurados en
XML. Para más información, consulte Procedimiento para generar XML a partir de archivos CSV (C#).
Los ejemplos de esta sección se dividen en dos categorías:
Vea también
Language Integrated Query (LINQ) (C#)
Procedimiento para generar XML a partir de archivos CSV
Procedimiento para realizar un recuento de las
repeticiones de una palabra en una cadena (LINQ)
(C#)
16/09/2021 • 2 minutes to read
En este ejemplo se muestra cómo usar una consulta LINQ para contar las apariciones de una palabra
determinada en una cadena. Observe que para realizar el recuento, primero se llama al método Split para crear
una matriz de palabras. Existe un costo de rendimiento en el método Split. Si la única operación de la cadena es
para contar las palabras, debe considerar la posibilidad de usar en su lugar los métodos Matches o IndexOf. Pero
si el rendimiento no es un problema crítico, o si ya ha dividido la frase para realizar otros tipos de consultas,
tiene sentido usar LINQ para además contar las palabras o frases.
Ejemplo
class CountWords
{
static void Main()
{
string text = @"Historically, the world of data and the world of objects" +
@" have not been well integrated. Programmers work in C# or Visual Basic" +
@" and also in SQL or XQuery. On the one side are concepts such as classes," +
@" objects, fields, inheritance, and .NET APIs. On the other side" +
@" are tables, columns, rows, nodes, and separate languages for dealing with" +
@" them. Data types often require translation between the two worlds; there are" +
@" different standard functions. Because the object world has no notion of query, a" +
@" query can only be represented as a string without compile-time type checking or" +
@" IntelliSense support in the IDE. Transferring data from SQL tables or XML trees to" +
@" objects in memory is often tedious and error-prone.";
Consulte también
LINQ y cadenas (C#)
Procedimiento para buscar frases que contengan un
conjunto especificado de palabras (LINQ) (C#)
16/09/2021 • 2 minutes to read
En este ejemplo se muestra cómo buscar frases en un archivo de texto que contengan coincidencias con cada
uno de los conjuntos de palabras especificados. Aunque la matriz de términos de búsqueda está codificada de
forma rígida en este ejemplo, también se podría rellenar dinámicamente en tiempo de ejecución. En este
ejemplo, la consulta devuelve las frases que contienen las palabras "Historically", "data" e "integrated".
Ejemplo
class FindSentences
{
static void Main()
{
string text = @"Historically, the world of data and the world of objects " +
@"have not been well integrated. Programmers work in C# or Visual Basic " +
@"and also in SQL or XQuery. On the one side are concepts such as classes, " +
@"objects, fields, inheritance, and .NET APIs. On the other side " +
@"are tables, columns, rows, nodes, and separate languages for dealing with " +
@"them. Data types often require translation between the two worlds; there are " +
@"different standard functions. Because the object world has no notion of query, a " +
@"query can only be represented as a string without compile-time type checking or " +
@"IntelliSense support in the IDE. Transferring data from SQL tables or XML trees to " +
@"objects in memory is often tedious and error-prone.";
// Define the search terms. This list could also be dynamically populated at runtime.
string[] wordsToMatch = { "Historically", "data", "integrated" };
// Find sentences that contain all the terms in the wordsToMatch array.
// Note that the number of terms to match is not specified at compile time.
var sentenceQuery = from sentence in sentences
let w = sentence.Split(new char[] { '.', '?', '!', ' ', ';', ':', ',' },
StringSplitOptions.RemoveEmptyEntries)
where w.Distinct().Intersect(wordsToMatch).Count() == wordsToMatch.Count()
select sentence;
La consulta primero divide el texto en frases y, luego, divide las frases en una matriz de cadenas que contienen
cada palabra. Para cada una de estas matrices, el método Distinct quita todas las palabras duplicadas y, después,
la consulta realiza una operación Intersect en la matriz de palabras y en la matriz wordsToMatch . Si el recuento
de la intersección es igual que el recuento de la matriz wordsToMatch , se han encontrado todas las palabras y se
devuelve la frase original.
En la llamada a Split, los signos de puntuación se usan como separadores para quitar las frases de la cadena. Si
no lo hiciera podría tener, por ejemplo, una cadena "Historically", lo que no coincidiría con el "Historically" de la
matriz wordsToMatch . Podría tener que usar separadores adicionales, en función de los tipos de puntuación del
texto de origen.
Compilar el código
Cree un proyecto de aplicación de consola de C# con directivas using para los espacios de nombres
System.Linq y System.IO.
Consulte también
LINQ y cadenas (C#)
Procedimiento para buscar caracteres en una
cadena (LINQ) (C#)
16/09/2021 • 2 minutes to read
Como la clase String implementa la interfaz IEnumerable<T> genérica, cualquier cadena puede consultarse
como una secuencia de caracteres. Pero esto no es un uso habitual de LINQ. Para operaciones de coincidencia de
patrones complejas, use la clase Regex.
Ejemplo
En el ejemplo siguiente se consulta una cadena para determinar el número de dígitos numéricos que contiene.
Tenga en cuenta que la consulta se "reutiliza" después de que se ejecute la primera vez. Esto es posible porque la
propia consulta no almacena ningún resultado real.
class QueryAString
{
static void Main()
{
string aString = "ABCDE99F-J74-12-89A";
Compilar el código
Cree un proyecto de aplicación de consola de C# con directivas using para los espacios de nombres
System.Linq y System.IO.
Consulte también
LINQ y cadenas (C#)
Procedimiento para combinar consultas LINQ con expresiones regulares (C#)
Procedimiento para combinar consultas LINQ con
expresiones regulares (C#)
16/09/2021 • 2 minutes to read
En este ejemplo se muestra cómo usar la clase Regex para crear una expresión regular para coincidencias más
complejas en cadenas de texto. Con la consulta LINQ, resulta fácil filtrar por los archivos exactos que se quieren
buscar con la expresión regular y dar forma a los resultados.
Ejemplo
class QueryWithRegEx
{
public static void Main()
{
// Modify this path as necessary so that it accesses your version of Visual Studio.
string startFolder = @"C:\Program Files (x86)\Microsoft Visual Studio 14.0\";
// One of the following paths may be more appropriate on your computer.
//string startFolder = @"C:\Program Files (x86)\Microsoft Visual Studio\2017\";
Tenga en cuenta que también puede consultar el objeto MatchCollection devuelto por una búsqueda RegEx . En
este ejemplo se genera solo el valor de cada coincidencia en los resultados. Pero también es posible usar LINQ
para realizar todo tipo de filtrado, ordenación y agrupación en esa colección. Dado que MatchCollection no es
una colección genérica IEnumerable, tendrá que indicar explícitamente el tipo de la variable de rango en la
consulta.
Compilar el código
Cree un proyecto de aplicación de consola de C# con directivas using para los espacios de nombres
System.Linq y System.IO.
Consulte también
LINQ y cadenas (C#)
LINQ y directorios de archivos (C#)
Procedimiento para buscar la diferencia de
conjuntos entre dos listas (LINQ) (C#)
16/09/2021 • 2 minutes to read
En este ejemplo se muestra cómo usar LINQ para comparar dos listas de cadenas y generar estas líneas, que
están en names1.txt pero no en names2.txt.
Para crear los archivos de datos
1. Copie names1.txt y names2.txt en la carpeta de la solución, como se muestra en Procedimiento para
combinar y comparar colecciones de cadenas (LINQ) (C#).
Ejemplo
class CompareLists
{
static void Main()
{
// Create the IEnumerable data sources.
string[] names1 = System.IO.File.ReadAllLines(@"../../../names1.txt");
string[] names2 = System.IO.File.ReadAllLines(@"../../../names2.txt");
// Create the query. Note that method syntax must be used here.
IEnumerable<string> differenceQuery =
names1.Except(names2);
Algunos tipos de operaciones de consulta en C#, como Except, Distinct, Union y Concat, solo pueden expresarse
en una sintaxis basada en método.
Compilar el código
Cree un proyecto de aplicación de consola de C# con directivas using para los espacios de nombres
System.Linq y System.IO.
Consulte también
LINQ y cadenas (C#)
Procedimiento para ordenar o filtrar datos de texto
por palabra o campo (LINQ) (C#)
16/09/2021 • 2 minutes to read
En el ejemplo siguiente se muestra cómo ordenar líneas de texto estructurado, como valores separados por
comas, por cualquier campo de la línea. El campo se puede especificar dinámicamente en tiempo de ejecución.
Supongamos que los campos de scores.csv representan el número de identificación de un alumno, seguido de
una serie de cuatro calificaciones.
Para crear un archivo que contenga datos
1. Copie los datos de scores.csv del tema Procedimiento para combinar contenido de archivos no similares
(LINQ) (C#).
Ejemplo
public class SortLines
{
static void Main()
{
// Create an IEnumerable data source
string[] scores = System.IO.File.ReadAllLines(@"../../../scores.csv");
return scoreQuery;
}
}
/* Output (if sortField == 1):
Sorted highest to lowest by field [1]:
116, 99, 86, 90, 94
120, 99, 82, 81, 79
111, 97, 92, 81, 60
114, 97, 89, 85, 82
121, 96, 85, 91, 60
122, 94, 92, 91, 91
117, 93, 92, 80, 87
118, 92, 90, 83, 78
113, 88, 94, 65, 91
112, 75, 84, 91, 39
119, 68, 79, 88, 92
115, 35, 72, 91, 70
*/
En este ejemplo también se muestra cómo devolver una variable de consulta desde un método.
Compilar el código
Cree un proyecto de aplicación de consola de C# con directivas using para los espacios de nombres
System.Linq y System.IO.
Consulte también
LINQ y cadenas (C#)
Procedimiento para reordenar los campos de un
archivo delimitado (LINQ) (C#)
16/09/2021 • 2 minutes to read
Un archivo de valores separados por comas (CSV) es un archivo de texto que se usa a menudo para almacenar
datos de hoja de cálculo u otros datos tabulares que se representan mediante filas y columnas. Mediante el uso
del método Split para separar los campos, es muy fácil consultar y manipular los archivos CSV con LINQ. De
hecho, la misma técnica puede usarse para reordenar los elementos de cualquier línea estructurada de texto, no
se limita a un archivo CSV.
En el ejemplo siguiente, suponga que las tres columnas representan el "apellido", el "nombre" y el "ID" de los
alumnos. Los campos están en orden alfabético según el apellido de los alumnos. La consulta genera una nueva
secuencia en la que la columna de identificador aparece en primer lugar, seguida por una segunda columna que
combina el nombre y el apellido del alumno. Las líneas se reordenan según el campo ID. Los resultados se
guardan en un archivo nuevo y no se modifican los datos originales.
Para crear el archivo de datos
1. Copie las líneas siguientes en un archivo de texto sin formato que se denomine spreadsheet1.csv. Guarde
el archivo en la carpeta del proyecto.
Adams,Terry,120
Fakhouri,Fadi,116
Feng,Hanying,117
Garcia,Cesar,114
Garcia,Debra,115
Garcia,Hugo,118
Mortensen,Sven,113
O'Donnell,Claire,112
Omelchenko,Svetlana,111
Tucker,Lance,119
Tucker,Michael,122
Zabokritski,Eugene,121
Ejemplo
class CSVFiles
{
static void Main(string[] args)
{
// Create the IEnumerable data source
string[] lines = System.IO.File.ReadAllLines(@"../../../spreadsheet1.csv");
// Execute the query and write out the new file. Note that WriteAllLines
// takes a string[], so ToArray is called on the query.
System.IO.File.WriteAllLines(@"../../../spreadsheet2.csv", query.ToArray());
Compilar el código
Cree un proyecto de aplicación de consola de C# con directivas using para los espacios de nombres
System.Linq y System.IO.
Consulte también
LINQ y cadenas (C#)
LINQ y directorios de archivos (C#)
Procedimiento para generar XML a partir de archivos CSV (C#)
Procedimiento para combinar y comparar
colecciones de cadenas (LINQ) (C#)
16/09/2021 • 2 minutes to read
En este ejemplo se muestra cómo combinar archivos que contienen líneas de texto y después ordenar los
resultados. En concreto, se muestra cómo realizar una concatenación simple, una unión y una intersección en los
dos conjuntos de líneas de texto.
Para configurar el proyecto y los archivos de texto
1. Copie estos nombres en un archivo de texto denominado names1.txt y guárdelo en la carpeta del
proyecto:
Bankov, Peter
Holm, Michael
Garcia, Hugo
Potra, Cristina
Noriega, Fabricio
Aw, Kam Foo
Beebe, Ann
Toyoshima, Tim
Guy, Wey Yuan
Garcia, Debra
2. Copie estos nombres en un archivo de texto denominado names2.txt y guárdelo en la carpeta del
proyecto. Tenga en cuenta que los dos archivos tienen algunos nombres en común.
Liu, Jinghao
Bankov, Peter
Holm, Michael
Garcia, Hugo
Beebe, Ann
Gilchrist, Beth
Myrcha, Jacek
Giakoumakis, Leo
McLin, Nkenge
El Yassir, Mehdi
Ejemplo
class MergeStrings
{
static void Main(string[] args)
{
//Put text files in your solution folder
string[] fileA = System.IO.File.ReadAllLines(@"../../../names1.txt");
string[] fileB = System.IO.File.ReadAllLines(@"../../../names2.txt");
IEnumerable<String> tempQuery1 =
from name in fileA
let n = name.Split(',')
where n[0] == nameMatch
select name;
IEnumerable<string> tempQuery2 =
from name2 in fileB
let n2 = name2.Split(',')
where n2[0] == nameMatch
select name2;
IEnumerable<string> nameMatchQuery =
tempQuery1.Concat(tempQuery2).OrderBy(s => s);
OutputQueryResults(nameMatchQuery, $"Concat based on partial name match \"{nameMatch}\":");
Compilar el código
Cree un proyecto de aplicación de consola de C# con directivas using para los espacios de nombres
System.Linq y System.IO.
Consulte también
LINQ y cadenas (C#)
LINQ y directorios de archivos (C#)
Procedimiento para rellenar colecciones de objetos
de varios orígenes (LINQ) (C#)
16/09/2021 • 4 minutes to read
En este ejemplo se muestra cómo combinar datos de orígenes diferentes en una secuencia de tipos nuevos.
NOTE
No intente unir datos en memoria o datos del sistema de archivos con datos que todavía están en una base de datos.
Dichas combinaciones entre dominios pueden producir resultados indefinidos porque hay diferentes maneras de definir
las operaciones de combinación para las consultas de base de datos y otros tipos de orígenes. Además, existe el riesgo de
que esta operación produzca una excepción de memoria insuficiente si la cantidad de datos existente en la base de datos
es considerable. Para combinar datos de una base de datos con datos en memoria, primero debe llamar a ToList o a
ToArray en la base de datos de consulta y, luego, debe efectuar la combinación en la colección devuelta.
Ejemplo
En el ejemplo siguiente se muestra cómo usar un tipo Student con nombre para almacenar los datos
combinados de dos colecciones de cadenas en memoria que simulan datos de hoja de cálculo en formato .csv.
La primera colección de cadenas representa los nombres y los identificadores de los estudiantes, mientras que
la segunda colección representa el identificador de los estudiantes (en la primera columna) y cuatro notas de
exámenes. El identificador se usa como clave externa.
using System;
using System.Collections.Generic;
using System.Linq;
class Student
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int ID { get; set; }
public List<int> ExamScores { get; set; }
}
class PopulateCollection
{
static void Main()
{
// These data files are defined in How to join content from
// dissimilar files (LINQ).
En la cláusula select se usa un inicializador de objeto para crear una instancia de cada objeto Student nuevo
usando los datos de los dos orígenes.
Si no tiene que almacenar los resultados de una consulta, los tipos anónimos pueden ser más convenientes que
los tipos con nombre. Los tipos con nombre son necesarios si pasa los resultados de la consulta fuera del
método en el que se ejecuta la consulta. En el ejemplo siguiente se ejecuta la misma tarea que en el ejemplo
anterior, con la diferencia de que se usan tipos anónimos en lugar de tipos con nombre:
// Merge the data sources by using an anonymous type.
// Note the dynamic creation of a list of ints for the
// ExamScores member. We skip 1 because the first string
// in the array is the student ID, not an exam score.
var queryNamesScores2 =
from nameLine in names
let splitName = nameLine.Split(',')
from scoreLine in scores
let splitScoreLine = scoreLine.Split(',')
where Convert.ToInt32(splitName[2]) == Convert.ToInt32(splitScoreLine[0])
select new
{
First = splitName[0],
Last = splitName[1],
ExamScores = (from scoreAsText in splitScoreLine.Skip(1)
select Convert.ToInt32(scoreAsText))
.ToList()
};
Consulte también
LINQ y cadenas (C#)
Inicializadores de objeto y colección
Tipos anónimos
Procedimiento para dividir un archivo en muchos
mediante grupos (LINQ) (C#)
16/09/2021 • 2 minutes to read
En este ejemplo se muestra una manera de combinar el contenido de dos archivos y luego crear un conjunto de
archivos nuevos que organicen los datos de una forma nueva.
Para crear los archivos de datos
1. Copie estos nombres en un archivo de texto denominado names1.txt y guárdelo en la carpeta del
proyecto:
Bankov, Peter
Holm, Michael
Garcia, Hugo
Potra, Cristina
Noriega, Fabricio
Aw, Kam Foo
Beebe, Ann
Toyoshima, Tim
Guy, Wey Yuan
Garcia, Debra
2. Copie estos nombres en un archivo de texto denominado names2.txt y guárdelo en la carpeta del
proyecto: tenga en cuenta que los dos archivos tienen algunos nombres en común.
Liu, Jinghao
Bankov, Peter
Holm, Michael
Garcia, Hugo
Beebe, Ann
Gilchrist, Beth
Myrcha, Jacek
Giakoumakis, Leo
McLin, Nkenge
El Yassir, Mehdi
Ejemplo
class SplitWithGroups
{
static void Main()
{
string[] fileA = System.IO.File.ReadAllLines(@"../../../names1.txt");
string[] fileB = System.IO.File.ReadAllLines(@"../../../names2.txt");
// Output to display.
Console.WriteLine(g.Key);
// Write file.
using (System.IO.StreamWriter sw = new System.IO.StreamWriter(fileName))
{
foreach (var item in g)
{
sw.WriteLine(item);
// Output to console for example purposes.
Console.WriteLine(" {0}", item);
}
}
}
// Keep console window open in debug mode.
Console.WriteLine("Files have been written. Press any key to exit");
Console.ReadKey();
}
}
/* Output:
A
Aw, Kam Foo
B
Bankov, Peter
Beebe, Ann
E
El Yassir, Mehdi
G
Garcia, Hugo
Guy, Wey Yuan
Garcia, Debra
Gilchrist, Beth
Giakoumakis, Leo
H
Holm, Michael
L
Liu, Jinghao
M
Myrcha, Jacek
McLin, Nkenge
N
Noriega, Fabricio
P
Potra, Cristina
T
Toyoshima, Tim
*/
El programa escribe un archivo independiente para cada grupo en la misma carpeta que los archivos de datos.
Compilar el código
Cree un proyecto de aplicación de consola de C# con directivas using para los espacios de nombres
System.Linq y System.IO.
Consulte también
LINQ y cadenas (C#)
LINQ y directorios de archivos (C#)
Procedimiento para combinar contenido de
archivos no similares (LINQ) (C#)
16/09/2021 • 2 minutes to read
En este ejemplo se muestra cómo combinar datos de dos archivos delimitados por comas que comparten un
valor común que se usa como clave coincidente. Esta técnica puede ser útil si tiene que combinar datos de dos
hojas de cálculo o si tiene que combinar en un archivo nuevo datos procedentes de una hoja de cálculo y de un
archivo que tiene otro formato. Puede modificar el ejemplo para adaptarlo a cualquier tipo de texto
estructurado.
2. Copie las líneas siguientes en un archivo llamado names.csv y guárdelo en la carpeta del proyecto. El
archivo representa una hoja de cálculo que contiene el nombre, los apellidos y el identificador de los
estudiantes.
Omelchenko,Svetlana,111
O'Donnell,Claire,112
Mortensen,Sven,113
Garcia,Cesar,114
Garcia,Debra,115
Fakhouri,Fadi,116
Feng,Hanying,117
Garcia,Hugo,118
Tucker,Lance,119
Adams,Terry,120
Zabokritski,Eugene,121
Tucker,Michael,122
Ejemplo
using System;
using System.Collections.Generic;
using System.Linq;
class JoinStrings
class JoinStrings
{
static void Main()
{
// Join content from dissimilar files that contain
// related information. File names.csv contains the student
// name plus an ID number. File scores.csv contains the ID
// and a set of four test scores. The following query joins
// the scores to the student names by using ID as a
// matching key.
En este ejemplo se muestra cómo efectuar cálculos agregados (como sumas, promedios, mínimos y máximos)
en las columnas de un archivo .csv. Los principios de ejemplo que se muestran aquí se pueden aplicar a otros
tipos de textos estructurados.
Ejemplo
class SumColumns
{
static void Main(string[] args)
{
string[] lines = System.IO.File.ReadAllLines(@"../../../scores.csv");
// Spreadsheet format:
// Student ID Exam#1 Exam#2 Exam#3 Exam#4
// 111, 97, 92, 81, 60
La consulta funciona usando el método Split para convertir cada línea de texto en una matriz. Cada elemento de
matriz representa una columna. Por último, el texto de cada columna se convierte en su representación
numérica. Si el archivo es un archivo separado por tabulaciones, actualice el argumento del método Split a
\t .
Compilar el código
Cree un proyecto de aplicación de consola de C# con directivas using para los espacios de nombres
System.Linq y System.IO.
Consulte también
LINQ y cadenas (C#)
LINQ y directorios de archivos (C#)
Procedimiento para consultar los metadatos de un
ensamblado con reflexión (LINQ) (C#)
16/09/2021 • 2 minutes to read
Las API de reflexión de .NET Framework se pueden usar para examinar los metadatos de un ensamblado .NET y
para crear colecciones de tipos, escribir miembros, parámetros y otros elementos que se encuentren en ese
ensamblado. Dado que estas colecciones admiten la interfaz genérica IEnumerable<T>, se pueden consultar
mediante LINQ.
En el ejemplo siguiente se muestra cómo se puede usar LINQ con reflexión para recuperar metadatos concretos
sobre métodos que coinciden con un criterio de búsqueda especificado. En este caso, la consulta encontrará los
nombres de todos los métodos del ensamblado que devuelven tipos enumerables, como matrices.
Ejemplo
using System;
using System.Linq;
using System.Reflection;
class ReflectionHowTO
{
static void Main()
{
Assembly assembly = Assembly.Load("System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=
b77a5c561934e089");
var pubTypesQuery = from type in assembly.GetTypes()
where type.IsPublic
from method in type.GetMethods()
where method.ReturnType.IsArray == true
|| ( method.ReturnType.GetInterface(
typeof(System.Collections.Generic.IEnumerable<>).FullName ) != null
&& method.ReturnType.FullName != "System.String" )
group method.ToString() by type.ToString();
El ejemplo usa el método Assembly.GetTypes para devolver una matriz de tipos en el ensamblado especificado.
Se aplica el filtro where para que solo se devuelvan tipos públicos. Para cada tipo público, se genera una
consulta anidada con la matriz MethodInfo que se devuelve desde la llamada Type.GetMethods. Estos resultados
se filtran para que solo devuelvan los métodos cuyo tipo de valor devuelto sea una matriz, o un tipo que
implemente IEnumerable<T>. Por último, estos resultados se agrupan usando el nombre de tipo como una
clave.
Vea también
LINQ to Objects (C#)
Procedimiento para consultar los metadatos de un
ensamblado con reflexión (LINQ) (C#)
16/09/2021 • 2 minutes to read
Las API de reflexión de .NET Framework se pueden usar para examinar los metadatos de un ensamblado .NET y
para crear colecciones de tipos, escribir miembros, parámetros y otros elementos que se encuentren en ese
ensamblado. Dado que estas colecciones admiten la interfaz genérica IEnumerable<T>, se pueden consultar
mediante LINQ.
En el ejemplo siguiente se muestra cómo se puede usar LINQ con reflexión para recuperar metadatos concretos
sobre métodos que coinciden con un criterio de búsqueda especificado. En este caso, la consulta encontrará los
nombres de todos los métodos del ensamblado que devuelven tipos enumerables, como matrices.
Ejemplo
using System;
using System.Linq;
using System.Reflection;
class ReflectionHowTO
{
static void Main()
{
Assembly assembly = Assembly.Load("System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=
b77a5c561934e089");
var pubTypesQuery = from type in assembly.GetTypes()
where type.IsPublic
from method in type.GetMethods()
where method.ReturnType.IsArray == true
|| ( method.ReturnType.GetInterface(
typeof(System.Collections.Generic.IEnumerable<>).FullName ) != null
&& method.ReturnType.FullName != "System.String" )
group method.ToString() by type.ToString();
El ejemplo usa el método Assembly.GetTypes para devolver una matriz de tipos en el ensamblado especificado.
Se aplica el filtro where para que solo se devuelvan tipos públicos. Para cada tipo público, se genera una
consulta anidada con la matriz MethodInfo que se devuelve desde la llamada Type.GetMethods. Estos resultados
se filtran para que solo devuelvan los métodos cuyo tipo de valor devuelto sea una matriz, o un tipo que
implemente IEnumerable<T>. Por último, estos resultados se agrupan usando el nombre de tipo como una
clave.
Vea también
LINQ to Objects (C#)
LINQ y directorios de archivos (C#)
16/09/2021 • 2 minutes to read
Muchas operaciones de sistema de archivos son esencialmente consultas y, por tanto, son adecuadas para el
enfoque LINQ.
Las consultas en esta sección no son destructivas. No se usan para cambiar el contenido de las carpetas o los
archivos originales. Esto sigue la regla de que las consultas no deben causar efectos secundarios. En general,
cualquier código (incluidas las consultas que ejecutan operadores de creación actualización y eliminación) que
modifica los datos de origen se debe separar del código que solo consulta los datos.
Esta sección contiene los siguientes temas:
Búsqueda de archivos con un nombre o atributo especificados (C#)
Muestra cómo buscar archivos examinando una o más propiedades de su objeto FileInfo.
Agrupación de archivos por extensión (LINQ) (C#)
Muestra cómo devolver grupos del objeto FileInfo basándose en su extensión de nombre de archivo.
Búsqueda del número total de bytes en un conjunto de carpetas (LINQ) (C#)
Muestra cómo devolver el número total de bytes en todos los archivos en un árbol de directorio especificado.
Procedimiento para comparar el contenido de dos carpetas (LINQ) (C#)
Muestra cómo devolver todos los archivos que se encuentran en dos carpetas especificadas y también todos los
archivos que se encuentran en una carpeta pero no en la otra.
Búsqueda del archivo o archivos de mayor tamaño en un árbol de directorios (LINQ) (C#)
Muestra cómo devolver el archivo mayor o menor, o un número especificado de archivos, en un árbol de
directorios.
Búsqueda de archivos duplicados en un árbol de directorios (LINQ) (C#)
Muestra cómo agrupar todos los nombres de archivo que aparecen en más de una ubicación en un árbol de
directorio especificado. También muestra cómo realizar comparaciones más complejas basadas en un
comparador personalizado.
Consulta del contenido de los archivos de una carpeta (LINQ) (C#)
Muestra cómo recorrer en iteración las carpetas de un árbol, abrir cada archivo y consultar el contenido del
archivo.
Comentarios
Hay cierta complejidad en la creación de un origen de datos que representa de forma precisa el contenido del
sistema de archivos y controla las excepciones correctamente. En los ejemplos de esta sección se crea una
colección de instantáneas de objetos FileInfo que representa todos los archivos en una carpeta raíz especificada
y todas sus subcarpetas. El estado real de cada FileInfo puede cambiar en el periodo comprendido entre el
comienzo y el fin de la ejecución de una consulta. Por ejemplo, se puede crear una lista de objetos FileInfo para
usarla como origen de datos. Si se intenta tener acceso a la propiedad Length en una consulta, el objeto FileInfo
intentará tener acceso al sistema de archivos para actualizar el valor de Length . Si el archivo ya no existe, se
obtendrá una excepción FileNotFoundException en la consulta, aunque no se esté consultando el sistema de
archivos directamente. Algunas consultas de esta sección usan un método independiente que consume estas
excepciones concretas en casos determinados. Otra opción consiste en mantener actualizado el origen de datos
de manera dinámica mediante FileSystemWatcher.
Vea también
LINQ to Objects (C#)
Procedimiento para buscar archivos con un nombre
o atributo especificados (C#)
16/09/2021 • 2 minutes to read
En este ejemplo se muestra cómo encontrar todos los archivos con una determinada extensión de nombre de
archivo (por ejemplo, ".txt") en un árbol de directorios especificado. También se muestra cómo devolver el
archivo más reciente o más antiguo del árbol por fecha de creación.
Ejemplo
class FindFileByExtension
{
// This query will produce the full path for all .txt files
// under the specified folder including subfolders.
// It orders the list according to the file name.
static void Main()
{
string startFolder = @"c:\program files\Microsoft Visual Studio 9.0\";
Vea también
LINQ to Objects (C#)
LINQ y directorios de archivos (C#)
Procedimiento para agrupar archivos por extensión
(LINQ) (C#)
16/09/2021 • 3 minutes to read
En este ejemplo se muestra cómo se puede usar LINQ para efectuar operaciones avanzadas de agrupación y
ordenación en listas de archivos o de carpetas. También muestra cómo paginar la salida en la ventana de
consola mediante los métodos Skip y Take.
Ejemplo
En la siguiente consulta se muestra cómo agrupar el contenido de un árbol de directorio especificado por la
extensión de nombre de archivo.
class GroupByExtension
{
// This query will sort all the files under the specified folder
// and subfolder into groups keyed by the file extension.
private static void Main()
{
// Take a snapshot of the file system.
string startFolder = @"c:\program files\Microsoft Visual Studio 9.0\Common7";
// This method specifically handles group queries of FileInfo objects with string keys.
// It can be modified to work for any long listings of data. Note that explicit typing
// must be used in method signatures. The groupbyExtList parameter is a query that produces
// groups of FileInfo objects with string keys.
private static void PageOutput(int rootLength,
IEnumerable<System.Linq.IGrouping<string, System.IO.FileInfo>>
groupByExtList)
{
// Flag to break out of paging loop.
bool goAgain = true;
// "3" = 1 line for extension + 1 for "Press any key" + 1 for input cursor.
int numLines = Console.WindowHeight - 3;
int numLines = Console.WindowHeight - 3;
// Output only as many lines of the current group as will fit in the window.
do
{
Console.Clear();
Console.WriteLine(filegroup.Key == String.Empty ? "[none]" : filegroup.Key);
if (goAgain == false)
break;
}
}
}
La salida de este programa puede ser larga, dependiendo de los detalles del sistema de archivos local y de la
configuración de startFolder . Para habilitar la visualización de todos los resultados, en este ejemplo se muestra
cómo paginar los resultados. Se pueden aplicar las mismas técnicas a las aplicaciones web y Windows. Observe
que, como el código pagina los elementos en un grupo, se necesita un bucle foreach anidado. También hay
alguna lógica adicional para calcular la posición actual en la lista y para permitir que el usuario detenga la
paginación y salga del programa. En este caso en concreto, la consulta de paginación se ejecuta en los
resultados almacenados en caché de la consulta original. En otros contextos, como en LINQ to SQL, este
almacenamiento en caché no es necesario.
Compilar el código
Cree un proyecto de aplicación de consola de C# con directivas using para los espacios de nombres
System.Linq y System.IO.
Vea también
LINQ to Objects (C#)
LINQ y directorios de archivos (C#)
Procedimiento para buscar el número total de bytes
de un conjunto de carpetas (LINQ) (C#)
16/09/2021 • 2 minutes to read
En este ejemplo se muestra cómo recuperar el número total de bytes usados por todos los archivos en una
carpeta especificada y en todas sus subcarpetas.
Ejemplo
El método Sum agrega los valores de todos los elementos seleccionados en la cláusula select . Puede modificar
fácilmente esta consulta para recuperar el archivo más grande y más pequeño en el árbol de directorio
especificado llamando al método Min o Max en lugar de Sum.
class QuerySize
{
public static void Main()
{
string startFolder = @"c:\program files\Microsoft Visual Studio 9.0\VC#";
// Return the total number of bytes in all the files under the specified folder.
long totalBytes = fileLengths.Sum();
Si solo tiene que contar el número de bytes en un árbol de directorio especificado, puede hacerlo de forma más
eficaz sin crear una consulta LINQ, ya que conlleva la sobrecarga de crear la colección de listas como un origen
de datos. La utilidad del enfoque de LINQ aumenta a medida que la consulta se vuelve más compleja, o cuando
se tienen que ejecutar varias consultas en el mismo origen de datos.
La consulta llama a un método independiente para obtener la longitud del archivo. Lo hace para consumir la
excepción que probablemente se producirá si el archivo se ha eliminado en otro subproceso después de que se
creara el objeto FileInfo en la llamada a GetFiles . Aunque ya se haya creado el objeto FileInfo, puede
producirse una excepción porque un objeto FileInfo intentará actualizar su propiedad Length con la longitud
más actual la primera vez que se tenga acceso a la propiedad. Al incluir esta operación en un bloque try-catch
fuera de la consulta, el código sigue la regla de evitar las operaciones en las consultas que pueden producir
efectos secundarios. En general, debe tener mucho cuidado al consumir excepciones para asegurarse de que no
deja una aplicación en un estado desconocido.
Compilar el código
Cree un proyecto de aplicación de consola de C# con directivas using para los espacios de nombres
System.Linq y System.IO.
Vea también
LINQ to Objects (C#)
LINQ y directorios de archivos (C#)
Procedimiento para comparar el contenido de dos
carpetas (LINQ) (C#)
16/09/2021 • 2 minutes to read
NOTE
Las técnicas que se mencionan aquí pueden adaptarse para comparar secuencias de objetos de cualquier tipo.
La clase FileComparer que aparece a continuación muestra cómo usar una clase de comparador personalizada
junto con los operadores de consulta estándar. La clase no está diseñada para su uso en escenarios reales.
Simplemente usa el nombre y la longitud en bytes de cada archivo para determinar si el contenido de cada una
de las carpetas es idéntico o no. En un escenario real, debería modificar este comparador para realizar una
comprobación de igualdad más rigurosa.
Ejemplo
namespace QueryCompareTwoDirs
{
class CompareDirs
{
if (queryCommonFiles.Any())
{
Console.WriteLine("The following files are in both folders:");
foreach (var v in queryCommonFiles)
{
Console.WriteLine(v.FullName); //shows which items end up in result list
}
}
else
{
Console.WriteLine("There are no common files in the two folders.");
}
Vea también
LINQ to Objects (C#)
LINQ y directorios de archivos (C#)
Procedimiento para buscar el archivo o archivos de
mayor tamaño en un árbol de directorios (LINQ)
(C#)
16/09/2021 • 3 minutes to read
En este ejemplo se muestran cinco consultas relacionadas con el tamaño de archivo en bytes:
Cómo recuperar el tamaño en bytes del archivo más grande.
Cómo recuperar el tamaño en bytes del archivo más pequeño.
Cómo recuperar el archivo de mayor o menor tamaño del objeto FileInfo de una o más carpetas en una
carpeta raíz especificada.
Cómo recuperar una secuencia, como los 10 archivos de mayor tamaño.
Cómo ordenar los archivos en grupos según su tamaño en bytes, sin incluir los archivos inferiores a un
tamaño especificado.
Ejemplo
El ejemplo siguiente contiene cinco consultas independientes que muestran cómo consultar y agrupar archivos,
en función de su tamaño en bytes. Puede modificar fácilmente estos ejemplos para basar la consulta en otra
propiedad del objeto FileInfo.
class QueryBySize
{
static void Main(string[] args)
{
QueryFilesBySize();
Console.WriteLine("Press any key to exit");
Console.ReadKey();
}
Console.WriteLine("The largest file under {0} is {1} with a length of {2} bytes",
startFolder, longestFile.FullName, longestFile.Length);
Console.WriteLine("The smallest file under {0} is {1} with a length of {2} bytes",
startFolder, smallestFile.FullName, smallestFile.Length);
Para devolver uno o más objetos FileInfo completos, la consulta debe examinar cada uno de ellos en los datos
de origen y, después, ordenarlos por el valor de su propiedad Length. Después, puede devolver el objeto único o
la secuencia con la mayor longitud. Use First para devolver el primer elemento de una lista. Use Take para
devolver el primer número n de elementos. Especifique un criterio de ordenación descendente para colocar los
elementos más pequeños al principio de la lista.
La consulta llama a un método independiente para obtener el tamaño del archivo en bytes para consumir la
posible excepción que se generará en el caso de que se haya eliminado un archivo en otro subproceso en el
período de tiempo desde que se ha creado el objeto FileInfo en la llamada a GetFiles . Aunque ya se haya
creado el objeto FileInfo, puede producirse una excepción porque un objeto FileInfo intentará actualizar su
propiedad Length con el tamaño más actual la primera vez que se tenga acceso a la propiedad. Al incluir esta
operación en un bloque try-catch fuera de la consulta, seguimos la regla de evitar las operaciones en las
consultas que pueden producir efectos secundarios. En general, debe tener mucho cuidado al consumir
excepciones para asegurarse de que no deja una aplicación en un estado desconocido.
Compilar el código
Cree un proyecto de aplicación de consola de C# con directivas using para los espacios de nombres
System.Linq y System.IO.
Vea también
LINQ to Objects (C#)
LINQ y directorios de archivos (C#)
Procedimiento para buscar archivos duplicados en
un árbol de directorios (LINQ) (C#)
16/09/2021 • 3 minutes to read
A veces, los archivos que tienen el mismo nombre pueden estar en más de una carpeta. Por ejemplo, en la
carpeta de instalación de Visual Studio, hay varias carpetas que tienen un archivo readme.htm. En este ejemplo
se muestra cómo buscar estos nombres de archivos duplicados en una carpeta raíz especificada. En el segundo
ejemplo se muestra cómo buscar archivos cuyo tamaño y fecha de LastWrite también coinciden.
Ejemplo
class QueryDuplicateFileNames
{
static void Main(string[] args)
{
// Uncomment QueryDuplicates2 to run that query.
QueryDuplicates();
// QueryDuplicates2();
int i = queryDupFiles.Count();
PageOutput<PortableKey, string>(queryDupFiles);
}
// "3" = 1 line for extension + 1 for "Press any key" + 1 for input cursor.
int numLines = Console.WindowHeight - 3;
// Output only as many lines of the current group as will fit in the window.
do
{
Console.Clear();
Console.WriteLine("Filename = {0}", filegroup.Key.ToString() == String.Empty ? "[none]" :
filegroup.Key.ToString());
if (goAgain == false)
break;
}
}
}
En la primera consulta se usa una clave simple para determinar una coincidencia; se buscan archivos que tengan
el mismo nombre, pero cuyo contenido podría ser diferente. En la segunda consulta se usa una clave compuesta
para coincidir con tres propiedades del objeto FileInfo. En esta consulta es mucho más probable que se
encuentren archivos que tienen el mismo nombre y un contenido similar o idéntico.
Compilar el código
Cree un proyecto de aplicación de consola de C# con directivas using para los espacios de nombres
System.Linq y System.IO.
Vea también
LINQ to Objects (C#)
LINQ y directorios de archivos (C#)
Procedimiento para consultar el contenido de los
archivos de texto de una carpeta (LINQ) (C#)
16/09/2021 • 2 minutes to read
En este ejemplo se muestra cómo consultar todos los archivos en un árbol de directorios especificado, abrir
cada archivo e inspeccionar su contenido. Este tipo de técnica puede usarse para crear índices o índices inversos
del contenido de un árbol de directorios. En este ejemplo, se realiza una búsqueda de cadena simple. Pero los
tipos más complejos de coincidencia de patrones se pueden realizar con una expresión regular. Para obtener
más información, vea Procedimiento para combinar consultas LINQ con expresiones regulares (C#).
Ejemplo
class QueryContents
{
public static void Main()
{
// Modify this path as necessary.
string startFolder = @"c:\program files\Microsoft Visual Studio 9.0\";
Compilar el código
Cree un proyecto de aplicación de consola de C# con directivas using para los espacios de nombres
System.Linq y System.IO.
Consulte también
LINQ y directorios de archivos (C#)
LINQ to Objects (C#)
Procedimiento para consultar un objeto ArrayList
con LINQ (C#)
16/09/2021 • 2 minutes to read
Cuando use LINQ para consultar colecciones no genéricas IEnumerable como ArrayList, debe declarar
explícitamente el tipo de variable de rango para reflejar el tipo específico de los objetos de la colección. Por
ejemplo, si tiene una ArrayList de objetos Student , la cláusula from debe tener un aspecto similar a este:
Ejemplo
En el siguiente ejemplo se muestra una consulta simple sobre un ArrayList. Tenga en cuenta que en este ejemplo
se usan inicializadores de objeto cuando el código llama al método Add, pero esto no es un requisito.
using System;
using System.Collections;
using System.Linq;
namespace NonGenericLINQ
{
public class Student
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int[] Scores { get; set; }
}
class Program
{
static void Main(string[] args)
{
ArrayList arrList = new ArrayList();
arrList.Add(
new Student
{
FirstName = "Svetlana", LastName = "Omelchenko", Scores = new int[] { 98, 92, 81, 60
}
});
arrList.Add(
new Student
{
FirstName = "Claire", LastName = "O’Donnell", Scores = new int[] { 75, 84, 91, 39 }
});
arrList.Add(
new Student
{
FirstName = "Sven", LastName = "Mortensen", Scores = new int[] { 88, 94, 65, 91 }
});
arrList.Add(
new Student
{
FirstName = "Cesar", LastName = "Garcia", Scores = new int[] { 97, 89, 85, 82 }
});
Vea también
LINQ to Objects (C#)
Procedimiento para agregar métodos
personalizados para las consultas LINQ (C#)
16/09/2021 • 5 minutes to read
Para extender el conjunto de métodos que usa para consultas LINQ, agregue métodos de extensión a la interfaz
IEnumerable<T>. Por ejemplo, además de las operaciones habituales de promedio o de máximo, puede crear un
método de agregación personalizado para calcular un solo valor a partir de una secuencia de valores. También
puede crear un método que funcione como un filtro personalizado o como una transformación de datos
específica para una secuencia de valores y que devuelva una nueva secuencia. Ejemplos de dichos métodos son
Distinct, Skip y Reverse.
Si extiende la interfaz IEnumerable<T>, puede aplicar los métodos personalizados a cualquier colección
enumerable. Para obtener más información, vea Métodos de extensión.
if (sortedList.Count % 2 == 0)
{
// Even number of items.
return (sortedList[itemIndex] + sortedList[itemIndex - 1]) / 2;
}
else
{
// Odd number of items.
return sortedList[itemIndex];
}
}
}
Puede llamar a este método de extensión para cualquier colección enumerable de la misma manera en la que
llamaría a otros métodos de agregación desde la interfaz IEnumerable<T>.
En el ejemplo de código siguiente se muestra cómo usar el método Median para una matriz de tipo double .
double[] numbers = { 1.9, 2, 8, 4, 5.7, 6, 7.2, 0 };
//int overload
public static double Median(this IEnumerable<int> source) =>
(from num in source select (double)num).Median();
Ahora puede llamar a las sobrecargas Median para los tipos integer y double , como se muestra en el código
siguiente:
int[] numbers2 = { 1, 2, 3, 4, 5 };
Ahora puede llamar al método Median para una secuencia de objetos de cualquier tipo. Si el tipo no tiene su
propia sobrecarga de métodos, deberá pasar un parámetro de delegado. En C# puede usar una expresión
lambda para este propósito. Además, solo en Visual Basic, si usa la cláusula Aggregate o Group By en lugar de
la llamada al método, puede pasar cualquier valor o expresión que esté en el ámbito de esta cláusula.
En el ejemplo de código siguiente se muestra cómo llamar al método Median para una matriz de enteros y una
matriz de cadenas. Para las cadenas, se calcula la mediana de las longitudes de las cadenas de la matriz. En el
ejemplo se muestra cómo pasar el parámetro del delegado Func<T,TResult> al método Median para cada caso.
int[] numbers3 = { 1, 2, 3, 4, 5 };
/*
You can use the num=>num lambda expression as a parameter for the Median method
so that the compiler will implicitly convert its value to double.
If there is no implicit conversion, the compiler will display an error message.
*/
var query3 = numbers3.Median(num => num);
// With the generic overload, you can also use numeric properties of objects.
/*
This code produces the following output:
Integer: Median = 3
String: Median = 4
*/
Puede llamar a este método de extensión para cualquier colección enumerable de la misma manera en la que
llamaría a otros métodos desde la interfaz IEnumerable<T>, como se muestra en el siguiente código:
a
c
e
*/
Consulte también
IEnumerable<T>
Métodos de extensión
LINQ to ADO.NET (Página de portal)
16/09/2021 • 2 minutes to read
LINQ to ADO.NET permite consultar sobre cualquier objeto enumerable de ADO.NET mediante el modelo de
programación de Language Integrated Query (LINQ).
NOTE
La documentación de LINQ to ADO.NET se encuentra en la sección ADO.NET del SDK de .NET Framework: LINQ y
ADO.NET.
Hay tres tecnologías de Language Integrated Query (LINQ) de ADO.NET independientes: LINQ to DataSet, LINQ
to SQL y LINQ to Entities. LINQ to DataSet proporciona consultas más ricas y optimizadas de DataSet, LINQ to
SQL permite consultar directamente los esquemas de base de datos de SQL Server y LINQ to Entities permite
consultar un modelo Entity Data Model.
LINQ to DataSet
DataSet es uno de los componentes más usados de ADO.NET, además de un elemento clave del modelo de
programación desconectado en el que se basa ADO.NET. En cambio, a pesar de su importancia, DataSet tiene
funciones de consulta limitadas.
LINQ to DataSet permite compilar capacidades de consulta más complejas en DataSet mediante la misma
funcionalidad de consulta disponible para muchos otros orígenes de datos.
Para más información, vea LINQ to DataSet.
LINQ to SQL
LINQ to SQL proporciona una infraestructura en tiempo de ejecución para administrar los datos relacionales
como objetos. En LINQ to SQL, el modelo de datos de una base de datos relacional se asigna a un modelo de
objetos expresado en el lenguaje de programación del desarrollador. Cuando se ejecuta la aplicación, LINQ to
SQL convierte a SQL las consultas integradas en el lenguaje en el modelo de objetos y las envía a la base de
datos para su ejecución. Cuando la base de datos devuelve los resultados, LINQ to SQL los vuelve a convertir en
objetos que se pueden manipular.
LINQ to SQL incluye compatibilidad con los procedimientos almacenados y las funciones definidas por el
usuario de la base de datos, además de con la herencia en el modelo de objetos.
Para más información, vea LINQ to SQL.
LINQ to Entities
A través del modelo Entity Data Model, los datos relacionales se exponen como objetos en el entorno .NET. Esto
hace de la capa de objetos un objetivo idóneo para la compatibilidad con LINQ, ya que permite a los
programadores formular consultas en la base de datos con el lenguaje usado para compilar la lógica
empresarial. Esta funcionalidad se conoce como LINQ to Entities. Para más información, vea LINQ to Entities.
Consulte también
LINQ y ADO.NET
Language Integrated Query (LINQ) (C#)
Habilitar un origen de datos para realizar consultas
LINQ
16/09/2021 • 3 minutes to read
Hay varias maneras de extender LINQ para permitir consultar cualquier origen de datos en el modelo LINQ El
origen de datos podría ser una estructura de datos, un servicio Web, un sistema de archivos o una base de
datos, por nombrar algunos. El modelo LINQ facilita a los clientes las consultas a un origen de datos para el que
las consultas LINQ están habilitadas, ya que la sintaxis y el modelo de consulta no cambian. Las maneras en las
que LINQ se puede extender a estos orígenes de datos son las siguientes:
Implementar la interfaz IEnumerable<T> en un tipo para habilitar las consultas de LINQ to Objects para
ese tipo.
Crear métodos de operador de consulta estándar como Where y Select que extienden un tipo para
habilitar las consultas personalizadas LINQ para ese tipo.
Crear un proveedor para el origen de datos que implementa la interfaz IQueryable<T>. Un proveedor
que implementa esta interfaz recibe consultas LINQ en forma de árboles de expresión, que puede
ejecutar de una manera personalizada, por ejemplo, remotamente.
Crear un proveedor para el origen de datos que aproveche una tecnología de LINQ existente. Este tipo de
proveedor permitiría no sólo las consultas, sino también las operaciones de inserción, actualización y
eliminación, así como la asignación para tipos definidos por el usuario.
En este tema se analizan estas opciones.
Consulte también
IQueryable<T>
IEnumerable<T>
Enumerable
Información general sobre operadores de consulta estándar (C#)
LINQ to Objects (C#)
Compatibilidad del IDE y las herramientas de Visual
Studio con LINQ (C#)
16/09/2021 • 2 minutes to read
El entorno de desarrollo integrado (IDE) de Visual Studio proporciona las siguientes características que admiten
el desarrollo de aplicaciones de LINQ:
Vea también
Language Integrated Query (LINQ) (C#)
Reflexión (C#)
16/09/2021 • 2 minutes to read
La reflexión proporciona objetos (de tipo Type) que describen los ensamblados, módulos y tipos. Puede usar la
reflexión para crear dinámicamente una instancia de un tipo, enlazar el tipo a un objeto existente u obtener el
tipo desde un objeto existente e invocar sus métodos, o acceder a sus campos y propiedades. Si usa atributos en
el código, la reflexión le permite acceder a ellos. Para obtener más información, consulte Attributes (Atributos).
Este es un ejemplo simple de reflexión que usa el método GetType(), heredado por todos los tipos de la clase
base Object , para obtener el tipo de una variable:
NOTE
Asegúrese de agregar using System; y using System.Reflection; en la parte superior del archivo de .cs.
El resultado es System.Int32 .
En el ejemplo siguiente se usa la reflexión para obtener el nombre completo del ensamblado cargado.
NOTE
Las palabras clave de C# protected y internal no tienen ningún significado en lenguaje intermedio (IL) y no se usan
en las API de reflexión. Los términos correspondientes en IL son Family y Assembly. Para identificar un método internal
con la reflexión, use la propiedad IsAssembly. Para identificar un método protected internal , use IsFamilyOrAssembly.
Secciones relacionadas
Para obtener más información:
Reflexión
Ver información tipos
Reflexión y tipos genéricos
System.Reflection.Emit
Recuperar la información almacenada en atributos
Consulte también
Guía de programación de C#
Ensamblados de .NET
Serialización (C#)
16/09/2021 • 4 minutes to read
La serialización es el proceso de convertir un objeto en una secuencia de bytes para almacenarlo o transmitirlo a
la memoria, a una base de datos o a un archivo. Su propósito principal es guardar el estado de un objeto para
poder volver a crearlo cuando sea necesario. El proceso inverso se denomina deserialización.
Funcionamiento de la serialización
En esta ilustración se muestra el proceso general de la serialización:
El objeto se serializa en una secuencia que incluye los datos. La secuencia también puede tener información
sobre el tipo del objeto, como la versión, la referencia cultural y el nombre del ensamblado. A partir de esa
secuencia, el objeto se puede almacenar en una base de datos, en un archivo o en memoria.
Usos de la serialización
La serialización permite al desarrollador guardar el estado de un objeto y volver a crearlo según sea necesario,
ya que proporciona almacenamiento de los objetos e intercambio de datos. A través de la serialización, un
desarrollador puede realizar acciones como las siguientes:
Enviar el objeto a una aplicación remota mediante un servicio web
Pasar un objeto de un dominio a otro
Pasar un objeto a través de un firewall como una cadena JSON o XML
Mantener la seguridad o información específica del usuario entre aplicaciones
Serialización de JSON
El espacio de nombres System.Text.Json contiene clases para la serialización y deserialización de notación de
objetos JavaScript (JSON). JSON es un estándar abierto que se usa normalmente para compartir datos en la
web.
La serialización de JSON serializa las propiedades públicas de un objeto en una cadena, una matriz de bytes o
una secuencia que se ajusta a la especificación de JSON RFC 8259. Para controlar la forma en que JsonSerializer
serializa o deserializa una instancia de la clase:
Use un objeto JsonSerializerOptions.
Aplique atributos del espacio de nombres System.Text.Json.Serialization a clases o propiedades.
Implemente convertidores personalizados.
WARNING
La serialización binaria puede ser peligrosa. Para obtener más información, consulte la Guía de seguridad BinaryFormatter.
La serialización XML serializa las propiedades y los campos públicos de un objeto o los parámetros y valores
devueltos de los métodos en una secuencia XML que se ajusta a un documento específico del lenguaje de
definición de esquema XML (XSD). La serialización XML produce clases fuertemente tipadas cuyas propiedades
y campos públicos se convierten a XML. System.Xml.Serialization contiene clases para serializar y deserializar
XML. Se aplican atributos a clases y a miembros de clase para controlar la forma en que XmlSerializer serializa o
deserializa una instancia de la clase.
Conversión de un objeto en serializable
Para la serialización binaria o XML, necesita lo siguiente:
Objeto que se va a serializar
Secuencia para incluir el objeto serializado
Instancia de System.Runtime.Serialization.Formatter
Aplique el atributo SerializableAttribute a un tipo para indicar que se pueden serializar instancias de este tipo. Si
se intenta serializar pero el tipo no tiene el atributo SerializableAttribute, se produce una excepción.
Para evitar que un campo se serialice, aplique el atributo NonSerializedAttribute. Si un campo de un tipo
serializable contiene un puntero, un controlador o alguna otra estructura de datos específica para un entorno
concreto y el campo no se puede reconstituir correctamente en un entorno diferente, puede convertirlo en no
serializable.
Si una clase serializada contiene referencias a objetos de otras clases marcadas como SerializableAttribute, esos
objetos también se serializarán.
Serialización básica y personalizada
La serialización binaria y XML se puede realizar de dos formas: de manera básica y personalizada.
La serialización básica utiliza .NET para serializar automáticamente el objeto. El único requisito es que la clase
tenga el atributo SerializableAttribute aplicado. NonSerializedAttribute puede usarse para impedir la
serialización de campos específicos.
Cuando se usa la serialización básica, el control de versiones de objetos puede causar problemas. Use la
serialización personalizada si los problemas de control de versiones son importantes. La serialización básica es
la manera más fácil de realizar la serialización, pero no proporciona mucho control sobre el proceso.
En la serialización personalizada, puede especificar exactamente qué objetos se serializarán y cómo se llevará a
cabo la serialización. La clase debe marcarse como SerializableAttribute e implementar la interfaz ISerializable.
Si quiere que el objeto también se deserialice de forma personalizada, use un constructor personalizado.
Serialización de diseñador
La serialización de diseñador es una forma especial de serialización que conlleva el tipo de persistencia de
objeto asociado a las herramientas de desarrollo. La serialización de diseñador es un proceso que consiste en
convertir un gráfico de objetos en un archivo de código fuente que puede utilizarse posteriormente para
recuperar el gráfico de objetos. Un archivo de código fuente puede contener código, marcado o incluso
información de la tabla SQL.
Temas relacionados y ejemplos
En Información general de System.Text.Json se muestra cómo obtener la biblioteca System.Text.Json .
En Procedimiento para serializar y deserializar JSON en .NET se muestra cómo leer y escribir datos de objetos a
y desde JSON mediante la clase JsonSerializer.
Tutorial: Conservar un objeto en Visual Studio (C#)
Se explica cómo se puede usar la serialización para conservar los datos de un objeto entre instancias, lo que le
permite almacenar valores y recuperarlos la próxima vez que se cree una instancia del objeto.
Procedimiento para leer datos de objeto de un archivo XML (C#)
Se muestra cómo leer los datos de objetos que se han escrito anteriormente en un archivo XML con la clase
XmlSerializer.
Procedimiento para escribir datos de objeto en un archivo XML (C#)
Se muestra cómo escribir el objeto de una clase en un archivo XML con la clase XmlSerializer.
Procedimiento para escribir datos de objeto en un
archivo XML (C#)
16/09/2021 • 2 minutes to read
En este ejemplo se escribe el objeto de una clase en un archivo XML con la clase XmlSerializer.
Ejemplo
public class XMLWrite
{
writer.Serialize(file, overview);
file.Close();
}
}
Compilar el código
La clase que se serializa debe tener un constructor público sin parámetros.
Programación sólida
Las condiciones siguientes pueden provocar una excepción:
La clase que se está serializando no tiene un constructor público sin parámetros.
El archivo ya existe y es de solo lectura (IOException).
La ruta de acceso del archivo es demasiado larga (PathTooLongException).
El disco está lleno (IOException).
Seguridad de .NET
En este ejemplo se crea un nuevo archivo, si este no existe aún. Si una aplicación necesita crear un archivo,
precisará acceso Create para la carpeta. Si el archivo ya existe, la aplicación necesitará solo acceso Write , un
privilegio menor. Siempre que sea posible, resulta más seguro crear el archivo durante la implementación y
conceder solo acceso Read a un único archivo, en lugar de acceso Create para una carpeta.
Vea también
StreamWriter
Procedimiento para leer datos de objeto de un archivo XML (C#)
Serialización (C#)
Procedimiento para leer datos de objeto de un
archivo XML (C#)
16/09/2021 • 2 minutes to read
En este ejemplo se leen los datos de objetos que se han escrito anteriormente en un archivo XML con la clase
XmlSerializer.
Ejemplo
public class Book
{
public String title;
}
Console.WriteLine(overview.title);
Compilar el código
Reemplace el nombre de archivo "c:\temp\SerializationOverview.xml" por el nombre del archivo que contiene
los datos serializados. Para más información sobre la serialización de datos, consulte Procedimiento para
escribir datos de objeto en un archivo XML (C#).
La clase debe tener un constructor público sin parámetros.
Solo se deserializan las propiedades y los campos públicos.
Programación sólida
Las condiciones siguientes pueden provocar una excepción:
La clase que se está serializando no tiene un constructor público sin parámetros.
Los datos del archivo no representan los datos de la clase que se va a deserializar.
El archivo no existe (IOException).
Seguridad de .NET
Compruebe siempre las entradas y nunca deserialice datos de un origen que no sea de confianza. El objeto que
se ha vuelto a crear se ejecuta en un equipo local con los permisos del código que lo ha deserializado.
Compruebe todas las entradas antes de utilizar los datos en la aplicación.
Vea también
StreamWriter
Procedimiento para escribir datos de objeto en un archivo XML (C#)
Serialización (C#)
Guía de programación de C#
Tutorial: Conservar un objeto con C#
16/09/2021 • 5 minutes to read
Puede usar la serialización para conservar los datos de un objeto entre instancias, lo que le permite almacenar
valores y recuperarlos la próxima vez que se cree una instancia del objeto.
En este tutorial, creará un objeto Loan básico y conservará sus datos en un archivo. Después, recuperará los
datos del archivo cuando vuelva a crear el objeto.
IMPORTANT
En este ejemplo se crea un nuevo archivo, si este no existe aún. Si una aplicación debe crear un archivo, es necesario que
tenga el permiso Create en la carpeta. Los permisos se establecen mediante el uso de las listas de control de acceso. Si
el archivo ya existe, la aplicación necesitará solo un permiso Write , un permiso menor. Siempre que sea posible, resulta
más seguro crear el archivo durante la implementación y conceder solo permisos Read a un único archivo (en lugar de
crear permisos para una carpeta). También es más seguro escribir datos en carpetas de usuario en lugar de hacerlo en la
carpeta raíz o en la carpeta Archivos de programa.
IMPORTANT
En este ejemplo se almacenan datos en un archivo de formato binario. Estos formatos no deben usarse para datos
confidenciales, como contraseñas o información de tarjetas de crédito.
Requisitos previos
Para compilar y ejecutar, instalar el SDK de .NET Core.
Instale el editor de código que prefiera si aún no lo ha hecho.
TIP
¿Es necesario instalar un editor de código? Pruebe Visual Studio.
[field:NonSerialized()]
public DateTime TimeLastLoaded { get; set; }
[field: NonSerialized()]
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
También tendrá que crear una aplicación que use la clase Loan .
Agregue un controlador de eventos del evento PropertyChanged y unas cuantas líneas para modificar el objeto
Loan y ver los cambios. En el siguiente código puede ver las adiciones realizadas:
Si ejecuta esta aplicación repetidamente, verá que siempre escribe los mismos valores. Se creará un objeto Loan
cada vez que ejecute el programa. En el mundo real, las tasas de interés cambian periódicamente, pero no
necesariamente cada vez que se ejecuta la aplicación. El código de serialización conlleva que se va a conservar la
tasa de interés más reciente entre las instancias de la aplicación. En el paso siguiente, hará esto agregando la
serialización a la clase Loan.
[Serializable()]
SerializableAttribute indica al compilador que todo el contenido de la clase se puede conservar en un archivo. El
evento PropertyChanged no representa la parte del gráfico de objeto que se debe almacenar, de modo que no se
debería serializar. Si lo hace, se serializarían todos los objetos que estén asociados a ese evento. Puede agregar
NonSerializedAttribute a la declaración de campo del controlador de eventos PropertyChanged .
[field: NonSerialized()]
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
A partir de C# 7.3, los atributos se pueden asociar al campo de respaldo de una propiedad implementada
automáticamente usando el valor de destino field . El siguiente código agrega una propiedad TimeLastLoaded
y la marca como no serializable:
[field:NonSerialized()]
public DateTime TimeLastLoaded { get; set; }
El siguiente paso consiste en agregar el código de serialización a la aplicación LoanApp. Para serializar la clase y
escribirla en un archivo, usaremos los espacios de nombres System.IO y
System.Runtime.Serialization.Formatters.Binary. Para evitar escribir los nombres completos, puede agregar
referencias a los espacios de nombres necesarios, tal y como se muestra en el siguiente código:
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
El siguiente paso es agregar código para deserializar el objeto del archivo cuando el objeto se crea. Agregue una
constante a la clase para el nombre de archivo de los datos serializados, tal y como se muestra en el siguiente
código:
Luego, agregue el siguiente código después de la línea que crea el objeto TestLoan :
if (File.Exists(FileName))
{
Console.WriteLine("Reading saved file");
Stream openFileStream = File.OpenRead(FileName);
BinaryFormatter deserializer = new BinaryFormatter();
TestLoan = (Loan)deserializer.Deserialize(openFileStream);
TestLoan.TimeLastLoaded = DateTime.Now;
openFileStream.Close();
}
Primero hay que confirmar que el archivo existe. Si existe, cree una clase Stream para leer el archivo binario y
una clase BinaryFormatter para traducirlo. También necesita convertir del tipo de secuencia al tipo de objeto
Loan.
Luego, debemos agregar código para serializar la clase en un archivo. Agregue el siguiente código después del
código existente en el método Main :
En este punto, podrá compilar y ejecutar la aplicación de nuevo. La primera vez que se ejecuta, observe que el
tipo de interés comienza en 7.5 y, después, pasa a 7.1. Cierre la aplicación y, después, ejecútela de nuevo. Ahora,
la aplicación muestra un mensaje que indica que se ha leído el archivo guardado y la tasa de interés es 7.1
incluso antes del código que la cambia.
Vea también
Serialización (C#)
Guía de programación de C#
Instrucciones, expresiones y operadores (Guía de
programación de C#)
16/09/2021 • 2 minutes to read
El código de C# que conforma una aplicación consta de instrucciones basadas en palabras clave, expresiones y
operadores. Esta sección contiene información sobre los elementos fundamentales de un programa de C#.
Para obtener más información, consulte:
Instrucciones
Operadores y expresiones
Miembros con forma de expresión
Funciones anónimas
Comparaciones de igualdad
Vea también
Guía de programación de C#
Conversiones de tipos
Instrucciones (Guía de programación de C#)
16/09/2021 • 6 minutes to read
Las acciones que realiza un programa se expresan en instrucciones. Entre las acciones comunes se incluyen
declarar variables, asignar valores, llamar a métodos, recorrer colecciones en bucle y crear una bifurcación a uno
u otro bloque de código, en función de una condición determinada. El orden en el que se ejecutan las
instrucciones en un programa se denomina flujo de control o flujo de ejecución. El flujo de control puede variar
cada vez que se ejecuta un programa, en función de cómo reacciona el programa a la entrada que recibe en
tiempo de ejecución.
Una instrucción puede constar de una sola línea de código que finaliza en un punto y coma o de una serie de
instrucciones de una sola línea en un bloque. Un bloque de instrucciones se incluye entre llaves {} y puede
contener bloques anidados. En el código siguiente se muestran dos ejemplos de instrucciones de una sola línea
y un bloque de instrucciones de varias líneas:
// Assignment statement.
counter = 1;
Instrucciones para el control de excepciones Las instrucciones para el control de excepciones permiten
recuperarse correctamente de condiciones excepcionales
producidas en tiempo de ejecución. Para obtener más
información, vea los temas siguientes:
throw
try-catch
try-finally
try-catch-finally
Instrucciones con etiqueta Puede asignar una etiqueta a una instrucción y, después,
usar la palabra clave goto para saltar a la instrucción con
etiqueta. (Vea el ejemplo de la línea siguiente).
Instrucciones de declaración
En el código siguiente se muestran ejemplos de declaraciones de variables con y sin una asignación inicial, y una
declaración constante con la inicialización necesaria.
Instrucciones de expresión
En el código siguiente se muestran ejemplos de instrucciones de expresión, que incluyen la asignación, la
creación de objetos con asignación y la invocación de método.
// Expression statement (assignment).
area = 3.14 * (radius * radius);
Instrucción vacía
En los ejemplos siguientes se muestran dos usos de una instrucción vacía:
void ProcessMessages()
{
while (ProcessMessage())
; // Statement needed here.
}
void F()
{
//...
if (done) goto exit;
//...
exit:
; // Statement needed here.
}
Instrucciones insertadas
Algunas instrucciones, por ejemplo, las instrucciones de iteración, siempre van seguidas de una instrucción
insertada. Esta instrucción insertada puede ser una sola instrucción o varias instrucciones incluidas entre llaves
{} en un bloque de instrucciones. Las instrucciones insertadas de una sola línea también pueden ir entre llaves {},
como se muestra en el siguiente ejemplo:
// Not recommended.
foreach (string s in System.IO.Directory.GetDirectories(
System.Environment.CurrentDirectory))
System.Console.WriteLine(s);
Una instrucción insertada que no está incluida entre llaves {} no puede ser una instrucción de declaración o una
instrucción con etiqueta. Esto se muestra en el ejemplo siguiente:
if(pointB == true)
//Error CS1023:
int radius = 5;
Coloque la instrucción insertada en un bloque para solucionar el error:
if (b == true)
{
// OK:
System.DateTime d = System.DateTime.Now;
System.Console.WriteLine(d.ToLongDateString());
}
Instrucciones inaccesibles
Si el compilador determina que el flujo de control no puede alcanzar nunca una instrucción determinada bajo
ninguna circunstancia, producirá una advertencia CS0162, como se muestra en el ejemplo siguiente:
Consulte también
Guía de programación de C#
Palabras clave de instrucciones
Operadores y expresiones de C#
Miembros con cuerpo de expresión (Guía de
programación de C#)
16/09/2021 • 4 minutes to read
M IEM B RO SE A DM IT E DESDE. . .
Método C# 6
Property C# 7.0
Constructor C# 7.0
Finalizador C# 7.0
Indizador C# 7.0
Métodos
Un método con cuerpo de expresión consta de una sola expresión que devuelve un valor cuyo tipo coincide con
el tipo de valor devuelto del método, o bien, para los métodos que devuelven void , que realiza alguna
operación. Por ejemplo, los tipos que reemplazan el método ToString normalmente incluyen una sola expresión
que devuelve la representación de cadena del objeto actual.
En el ejemplo siguiente se define una clase Person que reemplaza el método ToString con una definición de
cuerpo de expresión. También define un método DisplayName que muestra un nombre en la consola. Tenga en
cuenta que la palabra clave return no se usa en la definición de cuerpo de expresión de ToString .
using System;
class Example
{
static void Main()
{
Person p = new Person("Mandy", "Dejesus");
Console.WriteLine(p);
p.DisplayName();
}
}
En el ejemplo siguiente se define una clase Location cuya propiedad Name de solo lectura se implementa como
una definición de cuerpo de expresión que devuelve el valor del campo privado locationName :
Para más información sobre las propiedades, vea Propiedades (Guía de programación de C#).
Propiedades
A partir de C# 7.0, puede usar las definiciones de cuerpo de expresión para implementar los descriptores de
acceso get y set de propiedades. En el ejemplo siguiente se muestra cómo hacerlo:
public class Location
{
private string locationName;
Para más información sobre las propiedades, vea Propiedades (Guía de programación de C#).
Constructores
Una definición de cuerpo de expresión para un constructor normalmente consta de una expresión de asignación
única o una llamada de método que controla los argumentos del constructor o inicializa el estado de la
instancia.
En el ejemplo siguiente se define una clase Location cuyo constructor tiene un único parámetro de cadena
denominado name. La definición del cuerpo de expresión asigna el argumento a la propiedad Name .
Finalizadores
Una definición de cuerpo de expresión para un finalizador normalmente contiene instrucciones de limpieza,
como las instrucciones que liberan recursos no administrados.
En el ejemplo siguiente se define un finalizador que usa una definición de cuerpo de expresión para indicar que
el finalizador se ha llamado.
using System;
using System;
using System.Collections.Generic;
Una función anónima es una instrucción o expresión "alineada" que se puede usar siempre que se espera un
tipo delegado. Se puede usar para inicializar un delegado con nombre o se puede pasar como un parámetro de
método en lugar de un tipo delegado con nombre.
Puede usar una expresión lambda o un método anónimo para crear una función anónima. Se recomienda usar
expresiones lambda porque proporcionan una manera más concisa y expresiva para escribir código alineado. A
diferencia de los métodos anónimos, algunos tipos de expresiones lambda se pueden convertir en los tipos de
árbol de expresión.
Vea también
Instrucciones, expresiones y operadores
Expresiones lambda
Delegados
Árboles de expresión (C#)
Procedimiento Usar expresiones lambda en una
consulta (Guía de programación de C#)
16/09/2021 • 2 minutes to read
No se pueden usar expresiones lambda directamente en la sintaxis de consulta, pero sí en llamadas de método,
y las expresiones de consulta pueden contener llamadas de método. De hecho, algunas operaciones de consulta
solo se pueden expresar con la sintaxis de método. Para obtener más información sobre la diferencia entre la
sintaxis de consulta y la sintaxis de método, vea Sintaxis de consultas y sintaxis de métodos en LINQ.
Ejemplos
En el siguiente ejemplo se muestra cómo usar una expresión lambda en una consulta basada en métodos con el
operador de consulta estándar Enumerable.Where. Tenga en cuenta que el método Where de este ejemplo tiene
un parámetro de entrada de tipo delegado Func<T,TResult> y que el delegado toma un entero como entrada y
devuelve un valor booleano. La expresión lambda se puede convertir en ese delegado. Si se tratara de una
consulta de LINQ to SQL que usa el método Queryable.Where, el tipo de parámetro sería
Expression<Func<int,bool>> pero la expresión lambda tendría exactamente el mismo aspecto. Para más
información sobre el tipo Expression, vea System.Linq.Expressions.Expression.
class SimpleLambda
{
static void Main()
{
// Data source.
int[] scores = { 90, 71, 82, 93, 75, 82 };
En el ejemplo siguiente se muestra cómo usar una expresión lambda en una llamada de método de una
expresión de consulta. La expresión lambda es necesaria porque el operador de consulta estándar Sum no se
puede invocar con la sintaxis de consulta.
La consulta agrupa primero los alumnos por curso, tal y como se define en la enumeración GradeLevel . Luego,
suma las puntuaciones totales por alumno de cada grupo. Esto requiere dos operaciones Sum . La Sum interna
calcula la puntuación total de cada alumno y la Sum externa mantiene un total acumulado conjunto de todos los
alumnos del grupo.
private static void TotalsByGradeLevel()
{
// This query retrieves the total scores for First Year students, Second Years, and so on.
// The outer Sum method uses a lambda in order to specify which numbers to add together.
var categories =
from student in students
group student by student.Year into studentGroup
select new { GradeLevel = studentGroup.Key, TotalScore = studentGroup.Sum(s => s.ExamScores.Sum()) };
Compilar el código
Para ejecutar este código, copie y pegue el método en la clase StudentClass que se especifica en Consultar una
colección de objetos y llámelo desde el método Main .
Consulte también
Expresiones lambda
Árboles de expresión (C#)
Comparaciones de igualdad (guía de programación
de C#)
16/09/2021 • 4 minutes to read
A veces es necesario comparar si dos valores son iguales. En algunos casos, se prueba la igualdad de valores,
también denominada equivalencia, lo que significa que los valores contenidos en las dos variables son iguales.
En otros casos, hay que determinar si dos variables hacen referencia al mismo objeto subyacente de la memoria.
Este tipo de igualdad se denomina igualdad de referencia o identidad. En este tema se describen estos dos tipos
de igualdad y se proporcionan vínculos a otros temas para obtener más información.
Igualdad de referencia
La igualdad de referencia significa que dos referencias de objeto hacen referencia al mismo objeto subyacente.
Esto puede suceder mediante una asignación simple, como se muestra en el ejemplo siguiente.
using System;
class Test
{
public int Num { get; set; }
public string Str { get; set; }
// Assign b to a.
b = a;
En este código, se crean dos objetos, pero después de la instrucción de asignación, ambas referencias hacen
referencia al mismo objeto. Por consiguiente, presentan igualdad de referencia. Use el método ReferenceEquals
para determinar si dos referencias hacen referencia al mismo objeto.
El concepto de igualdad de la referencia solo se aplica a los tipos de referencia. Los objetos de tipo de valor no
pueden presentar igualdad de referencia porque al asignar una instancia de un tipo de valor a una variable, se
realiza una copia del valor. Por consiguiente, nunca puede haber dos structs con conversión unboxing que hagan
referencia a la misma ubicación de la memoria. Además, si se usa ReferenceEquals para comparar dos tipos de
valor, el resultado siempre será false , aunque todos los valores que contengan los objetos sean idénticos. Esto
se debe a que a cada variable se aplica la conversión boxing en una instancia de objeto independiente. Para más
información, consulte Procedimiento Probar la igualdad de referencia (Identidad).
Igualdad de valores
La igualdad de valores significa que dos objetos contienen el mismo valor o valores. Para los tipos de valor
primitivos, como int o bool, las pruebas de igualdad de valores son sencillas. Puede usar el operador ==, como
se muestra en el ejemplo siguiente.
int a = GetOriginalValue();
int b = GetCurrentValue();
Para la mayoría de los otros tipos, las pruebas de igualdad de valores son más complejas, porque es preciso
entender cómo la define el tipo. Para las clases y los structs que tienen varios campos o propiedades, la igualdad
de valores suele definirse de modo que significa que todos los campos o propiedades tienen el mismo valor. Por
ejemplo, podrían definirse dos objetos Point que fueran equivalentes si pointA.X es igual a pointB.X y pointA.Y
es igual a pointB.Y. En el caso de los registros, la igualdad de valores significa que dos variables de un tipo de
registro son iguales si los tipos coinciden y todos los valores de propiedad y campo coinciden.
En cambio, no hay ningún requisito que exija que la equivalencia se base en todos los campos de un tipo. Se
puede basar en un subconjunto. Al comparar tipos que no sean de su propiedad, es importante asegurarse
concretamente de cómo se define la equivalencia para ese tipo. Para más información sobre cómo definir la
igualdad de valores en sus propias clases y structs, consulte Procedimiento Definir la igualdad de valores para
un tipo.
Igualdad de valores en valores de número de punto flotante
Las comparaciones de igualdad de valores de punto flotante (double y float) son problemáticas debido a la
imprecisión de la aritmética de número de punto flotante en los equipos binarios. Para obtener más
información, vea los comentarios en el tema System.Double.
Temas relacionados
T IT L E DESC RIP C IÓ N
Procedimiento Probar la igualdad de referencia (Identidad) Describe cómo determinar si dos variables presentan
igualdad de referencia.
Procedimiento Definir la igualdad de valores para un tipo Describe cómo proporcionar una definición personalizada de
igualdad de valores para un tipo.
Los registros implementan automáticamente la igualdad de valores. Considere la posibilidad de definir record
en lugar de class cuando el tipo modela los datos y debe implementar la igualdad de valores.
Cuando defina una clase o un struct, debe decidir si tiene sentido crear una definición personalizada de igualdad
(o equivalencia) de valores para el tipo. Normalmente, la igualdad de valores se implementa cuando se espera
agregar objetos del tipo a una colección, o cuando su objetivo principal es almacenar un conjunto de campos o
propiedades. Puede basar la definición de la igualdad de valores en una comparación de todos los campos y
propiedades del tipo, o bien puede basarla en un subconjunto.
En cualquier caso, tanto en las clases como en las estructuras, la implementación debe cumplir las cinco
garantías de equivalencia (en las siguientes reglas, se da por hecho que x , y y z no son NULL):
1. La propiedad reflexiva x.Equals(x) devuelve true .
2. La propiedad simétrica x.Equals(y) devuelve el mismo valor que y.Equals(x) .
3. La propiedad transitiva: si (x.Equals(y) && y.Equals(z)) devuelve true , x.Equals(z) devuelve true .
4. Las invocaciones sucesivas de x.Equals(y) devuelven el mismo valor siempre y cuando los objetos a los
que x e y hacen referencia no se modifiquen.
5. Cualquier valor distinto de NULL no es igual a NULL. Sin embargo, x.Equals(y) produce una excepción
cuando x es NULL. Esto rompe las reglas 1 o 2, en función del argumento de Equals .
Cualquier struct que defina ya tiene una implementación predeterminada de igualdad de valor que hereda de la
invalidación System.ValueType del método Object.Equals(Object). Esta implementación usa la reflexión para
examinar todos los campos y propiedades del tipo. Aunque esta implementación genera resultados correctos, es
relativamente lenta en comparación con una implementación personalizada escrita específicamente para el tipo.
Los detalles de implementación para la igualdad de valores son diferentes para las clases y los structs. A pesar
de ello, tanto las clases como los structs requieren los mismos pasos básicos para implementar la igualdad:
1. Invalide el método virtual Object.Equals(Object). En la mayoría de los casos, la implementación de
bool Equals( object obj ) debería llamar solamente al método Equals específico del tipo que es la
implementación de la interfaz System.IEquatable<T>. (Vea el paso 2).
2. Implemente la interfaz System.IEquatable<T> proporcionando un método Equals específico del tipo.
Aquí es donde se realiza la comparación de equivalencias propiamente dicha. Por ejemplo, podría decidir
que, para definir la igualdad, solo se comparen uno o dos campos del tipo. No genere excepciones desde
Equals . Para las clases que están relacionadas por herencia:
este método debe examinar únicamente los campos que se declaran en la clase. Debe llamar a
base.Equals para examinar los campos que están en la clase base. (No llame a base.Equals si el
tipo hereda directamente de Object, porque la implementación Object de Object.Equals(Object)
realiza una comprobación de igualdad de referencia).
Dos variables deben considerarse iguales solo si los tipos en tiempo de ejecución de las variables
que se van a comparar son los mismos. Además, asegúrese de que se utiliza la implementación
IEquatable del método Equals para el tipo en tiempo de ejecución si los tipos en tiempo de
ejecución y en tiempo de compilación de una variable son diferentes. Una estrategia para
asegurarse de que los tipos en tiempo de ejecución siempre se comparan correctamente es
implementar IEquatable solo en clases sealed . Para obtener más información, vea el ejemplo de
clases más adelante en este artículo.
3. Opcional, pero recomendado: Sobrecargue los operadores == y !=.
4. Invalide Object.GetHashCode de manera que dos objetos que tengan igualdad de valor produzcan el
mismo código hash.
5. Opcional: Para admitir definiciones para "mayor que" o "menor que", implemente la interfaz
IComparable<T> para el tipo y sobrecargue también los operadores <= y >=.
NOTE
A partir de C# 9.0, puede usar registros para obtener la semántica de igualdad de valores sin código reutilizable
innecesario.
Ejemplo de clase
En el ejemplo siguiente se muestra cómo implementar la igualdad de valores en una clase (tipo de referencia).
using System;
namespace ValueEqualityClass
{
class TwoDPoint : IEquatable<TwoDPoint>
{
public int X { get; private set; }
public int Y { get; private set; }
public static bool operator !=(TwoDPoint lhs, TwoDPoint rhs) => !(lhs == rhs);
}
public static bool operator !=(ThreeDPoint lhs, ThreeDPoint rhs) => !(lhs == rhs);
}
class Program
{
static void Main(string[] args)
{
ThreeDPoint pointA = new ThreeDPoint(3, 4, 5);
ThreeDPoint pointB = new ThreeDPoint(3, 4, 5);
ThreeDPoint pointC = null;
int i = 5;
/* Output:
pointA.Equals(pointB) = True
pointA == pointB = True
null comparison = False
Compare to some other type = False
Two null TwoDPoints are equal: True
(pointE == pointA) = False
(pointA == pointE) = False
(pointA != pointE) = True
pointE.Equals(list[0]): False
*/
}
En las clases (tipos de referencia), la implementación predeterminada de ambos métodos Object.Equals(Object)
realiza una comparación de igualdad de referencia, no una comprobación de igualdad de valores. Cuando un
implementador invalida el método virtual, lo hace para asignarle semántica de igualdad de valores.
Los operadores == y != pueden usarse con clases, incluso si la clase no los sobrecarga, pero el
comportamiento predeterminado consiste en realizar una comprobación de igualdad de referencia. En una
clase, si sobrecarga el método Equals , debería sobrecargar los operadores == y != , pero no es obligatorio.
IMPORTANT
Es posible que el código de ejemplo anterior no controle cada escenario de herencia de la manera esperada. Observe el
código siguiente:
Este código notifica que p1 es igual a p2 pesar de la diferencia en los valores z . La diferencia se omite porque el
compilador elige la implementación TwoDPoint de IEquatable basándose en el tipo en tiempo de compilación.
La igualdad de valores integrada de los tipos record controla escenarios como este. Si TwoDPoint y ThreeDPoint
fueran de tipo record , el resultado de p1.Equals(p2) sería False . Para obtener más información, vea Igualdad en las
jerarquías de herencia de tipo record .
Ejemplo de estructura
En el ejemplo siguiente se muestra cómo implementar la igualdad de valores en un struct (tipo de valor):
using System;
namespace ValueEqualityStruct
{
struct TwoDPoint : IEquatable<TwoDPoint>
{
public int X { get; private set; }
public int Y { get; private set; }
public override bool Equals(object obj) => obj is TwoDPoint other && this.Equals(other);
public static bool operator ==(TwoDPoint lhs, TwoDPoint rhs) => lhs.Equals(rhs);
public static bool operator !=(TwoDPoint lhs, TwoDPoint rhs) => !(lhs == rhs);
}
class Program
{
{
static void Main(string[] args)
{
TwoDPoint pointA = new TwoDPoint(3, 4);
TwoDPoint pointB = new TwoDPoint(3, 4);
int i = 5;
// True:
Console.WriteLine("pointA.Equals(pointB) = {0}", pointA.Equals(pointB));
// True:
Console.WriteLine("pointA == pointB = {0}", pointA == pointB);
// True:
Console.WriteLine("object.Equals(pointA, pointB) = {0}", object.Equals(pointA, pointB));
// False:
Console.WriteLine("pointA.Equals(null) = {0}", pointA.Equals(null));
// False:
Console.WriteLine("(pointA == null) = {0}", pointA == null);
// True:
Console.WriteLine("(pointA != null) = {0}", pointA != null);
// False:
Console.WriteLine("pointA.Equals(i) = {0}", pointA.Equals(i));
// CS0019:
// Console.WriteLine("pointA == i = {0}", pointA == i);
pointD = temp;
// True:
Console.WriteLine("pointD == (pointC = 3,4) = {0}", pointD == pointC);
/* Output:
pointA.Equals(pointB) = True
pointA == pointB = True
Object.Equals(pointA, pointB) = True
pointA.Equals(null) = False
(pointA == null) = False
(pointA != null) = True
pointA.Equals(i) = False
pointE.Equals(list[0]): True
pointA == (pointC = null) = False
pointC == pointD = True
pointA == (pointC = 3,4) = True
pointD == (pointC = 3,4) = True
*/
}
Para los structs, la implementación predeterminada de Object.Equals(Object) (que es la versión invalidada de
System.ValueType) realiza una comprobación de igualdad de valor con la reflexión para comparar valores de
cada campo en el tipo. Cuando un implementador invalida el método Equals virtual en un struct, lo hace para
proporcionar un medio más eficaz de llevar a cabo la comprobación de igualdad de valores y, opcionalmente,
para basar la comparación en un subconjunto de propiedades o campos del struct.
Los operadores == y != no pueden funcionar en un struct a menos que el struct los sobrecargue explícitamente.
Consulte también
Comparaciones de igualdad
Guía de programación de C#
Procedimiento Probar la igualdad de referencias
(identidad) (Guía de programación de C#)
16/09/2021 • 3 minutes to read
No tiene que implementar ninguna lógica personalizada para admitir las comparaciones de igualdad de
referencias en los tipos. Esta funcionalidad se proporciona para todos los tipos mediante el método
Object.ReferenceEquals estático.
En el ejemplo siguiente, se muestra cómo determinar si dos variables tienen igualdad de referencia, lo que
significa que hacen referencia al mismo objeto en la memoria.
En el ejemplo también se muestra por qué Object.ReferenceEquals siempre devuelve false para los tipos de
valor y por qué no se debe usar ReferenceEquals para determinar la igualdad entre cadenas.
Ejemplo
using System;
using System.Text;
namespace TestReferenceEquality
{
struct TestStruct
{
public int Num { get; private set; }
public string Name { get; private set; }
class TestClass
{
public int Num { get; set; }
public string Name { get; set; }
}
class Program
{
static void Main()
{
// Demonstrate reference equality with reference types.
#region ReferenceTypes
// Demonstrate that two value type instances never have reference equality.
#region ValueTypes
#region stringRefEquality
// Constant strings within the same assembly are always interned by the runtime.
// This means they are stored in the same location in memory. Therefore,
// the two strings have reference equality although no assignment takes place.
string strA = "Hello world!";
string strB = "Hello world!";
Console.WriteLine("ReferenceEquals(strA, strB) = {0}",
Object.ReferenceEquals(strA, strB)); // true
#endregion
/* Output:
ReferenceEquals(tcA, tcB) = False
After assignment: ReferenceEquals(tcA, tcB) = True
tcB.Name = TestClass 42 tcB.Num: 42
After assignment: ReferenceEquals(tsC, tsD) = False
ReferenceEquals(strA, strB) = True
strA = "Goodbye world!" strB = "Hello world!"
After strA changes, ReferenceEquals(strA, strB) = False
ReferenceEquals(stringC, strB) = False
stringC == strB = True
*/
La implementación de Equals en la clase base universal System.Object también realiza una comprobación de la
igualdad de referencias; sin embargo, es mejor no seguir este procedimiento porque, en el caso de que una
clase invalide el método, los resultados podrían no ser los esperados. Lo mismo se cumple para los operadores
== y != . Cuando se usan en tipos de referencia, el comportamiento predeterminado de == y != consiste en
realizar una comprobación de la igualdad de referencias. Sin embargo, las clases derivadas pueden sobrecargar
el operador para realizar una comprobación de la igualdad de valores. Para minimizar las posibilidades de error,
lo mejor es usar siempre ReferenceEquals cuando deba determinarse si dos objetos tienen igualdad de
referencia.
El runtime siempre aplica el método Intern a las cadenas constantes dentro del mismo ensamblado. Es decir,
solo se conserva una instancia de cada cadena literal única. Sin embargo, el runtime no garantiza que se vaya a
aplicar el método Intern a las cadenas creadas en tiempo de ejecución, ni tampoco que dicho método se aplique
a dos cadenas constantes iguales en distintos ensamblados.
Consulte también
Comparaciones de igualdad
Conversiones de tipos (Guía de programación de
C#)
16/09/2021 • 5 minutes to read
Dado que C# tiene tipos estáticos en tiempo de compilación, después de declarar una variable, no se puede
volver a declarar ni se le puede asignar un valor de otro tipo a menos que ese tipo sea convertible de forma
implícita al tipo de la variable. Por ejemplo, string no se puede convertir de forma implícita a int . Por tanto,
después de declarar i como un valor int , no se le puede asignar la cadena "Hello", como se muestra en el
código siguiente:
int i;
Pero es posible que en ocasiones sea necesario copiar un valor en una variable o parámetro de método de otro
tipo. Por ejemplo, es posible que tenga una variable de entero que se necesita pasar a un método cuyo
parámetro es de tipo double . O es posible que tenga que asignar una variable de clase a una variable de tipo de
interfaz. Estos tipos de operaciones se denominan conversiones de tipos. En C#, se pueden realizar las siguientes
conversiones de tipos:
Conversiones implícitas : no se requiere ninguna sintaxis especial porque la conversión siempre es
correcta y no se perderá ningún dato. Los ejemplos incluyen conversiones de tipos enteros más
pequeños a más grandes, y conversiones de clases derivadas a clases base.
Conversiones explícitas : las conversiones explícitas requieren una expresión Cast. La conversión es
necesaria si es posible que se pierda información en la conversión, o cuando es posible que la conversión
no sea correcta por otros motivos. Entre los ejemplos típicos están la conversión numérica a un tipo que
tiene menos precisión o un intervalo más pequeño, y la conversión de una instancia de clase base a una
clase derivada.
Conversiones definidas por el usuario : las conversiones definidas por el usuario se realizan por
medio de métodos especiales que se pueden definir para habilitar las conversiones explícitas e implícitas
entre tipos personalizados que no tienen una relación de clase base-clase derivada. Para obtener más
información, vea Operadores de conversión definidos por el usuario.
Conversiones con clases del asistente : para realizar conversiones entre tipos no compatibles, como
enteros y objetos System.DateTime, o cadenas hexadecimales y matrices de bytes puede usar la clase
System.BitConverter, la clase System.Convert y los métodos Parse de los tipos numéricos integrados,
como Int32.Parse. Para obtener más información, consulte Procedimiento Convertir una matriz de bytes
en un valor int, Procedimiento Convertir una cadena en un número y Procedimiento Convertir cadenas
hexadecimales en tipos numéricos.
Conversiones implícitas
Para los tipos numéricos integrados, se puede realizar una conversión implícita cuando el valor que se va a
almacenar se puede encajar en la variable sin truncarse ni redondearse. Para los tipos enteros, esto significa que
el intervalo del tipo de origen es un subconjunto apropiado del intervalo para el tipo de destino. Por ejemplo,
una variable de tipo long (entero de 64 bits) puede almacenar cualquier valor que un tipo int (entero de 32 bits)
pueda almacenar. En el ejemplo siguiente, el compilador convierte de forma implícita el valor de num en la
parte derecha a un tipo long antes de asignarlo a bigNum .
Para obtener una lista completa de las conversiones numéricas implícitas, consulte la sección Conversiones
numéricas implícitas del artículo Conversiones numéricas integradas.
Para los tipos de referencia, siempre existe una conversión implícita desde una clase a cualquiera de sus
interfaces o clases base directas o indirectas. No se necesita ninguna sintaxis especial porque una clase derivada
siempre contiene a todos los miembros de una clase base.
// Always OK.
Base b = d;
Conversiones explícitas
Pero si no se puede realizar una conversión sin riesgo de perder información, el compilador requiere que se
realice una conversión explícita, que se denomina conversión. Una conversión de tipos es una manera de
informar explícitamente al compilador de que se pretende realizar la conversión y se es consciente de que se
puede producir pérdida de datos o la conversión de tipos puede fallar en el tiempo de ejecución. Para realizar
una conversión, especifique el tipo al que se va a convertir entre paréntesis delante del valor o la variable que se
va a convertir. El siguiente programa convierte un tipo double en un tipo int. El programa no se compilará sin la
conversión.
class Test
{
static void Main()
{
double x = 1234.7;
int a;
// Cast double to int.
a = (int)x;
System.Console.WriteLine(a);
}
}
// Output: 1234
Para obtener una lista completa de las conversiones numéricas explícitas admitidas, consulte la sección
Conversiones numéricas explícitas del artículo Conversiones numéricas integradas.
Para los tipos de referencia, se requiere una conversión explícita si es necesario convertir de un tipo base a un
tipo derivado:
// Create a new derived type.
Giraffe g = new Giraffe();
Una operación de conversión entre tipos de referencia no cambia el tipo en tiempo de ejecución del objeto
subyacente. Solo cambia el tipo del valor que se usa como referencia a ese objeto. Para obtener más
información, vea Polimorfismo .
class Animal
{
public void Eat() => System.Console.WriteLine("Eating.");
class UnSafeCast
{
static void Main()
{
Test(new Mammal());
El método Test tiene un parámetro Animal , por lo que la conversión explícita del argumento a en un
Reptile supone una suposición peligrosa. Es más seguro no hacer suposiciones, sino comprobar el tipo. C#
proporciona el operador is para permitir probar la compatibilidad antes de realizar una conversión. Para obtener
más información, consulte Procedimiento para convertir de forma segura mediante la coincidencia de patrones
y los operadores is y as.
Vea también
Guía de programación de C#
Tipos
Expresión Cast
Operadores de conversión definidos por el usuario
Conversión de tipos generalizada
Procedimiento Convertir una cadena en un número
Conversión boxing y unboxing (Guía de
programación de C#)
16/09/2021 • 4 minutes to read
La conversión boxing es el proceso de convertir un tipo de valor en el tipo object o en cualquier tipo de
interfaz implementado por este tipo de valor. Cuando Common Language Runtime (CLR) aplica la conversión
boxing a un tipo de valor, ajusta el valor dentro de una instancia System.Object y lo almacena en el montón
administrado. La conversión unboxing extrae el tipo de valor del objeto. La conversión boxing es implícita y la
conversión unboxing es explícita. El concepto de conversión boxing y unboxing es la base de la vista unificada
del sistema de tipos de C#, en el que un valor de cualquier tipo se puede tratar como objeto.
En el ejemplo siguiente, se aplica conversión boxing a la variable de entero i y esta se asigna al objeto o .
int i = 123;
// The following line boxes i.
object o = i;
o = 123;
i = (int)o; // unboxing
/*Output:
646F74636574
*/
Rendimiento
Con relación a las asignaciones simples, las conversiones boxing y unboxing son procesos que consumen
muchos recursos. Cuando se aplica la conversión boxing a un tipo de valor, se debe asignar y construir un objeto
completamente nuevo. En menor grado, la conversión de tipos requerida para aplicar la conversión unboxing
también es costosa. Para más información, vea Rendimiento.
Boxing
La conversión boxing se utiliza para almacenar tipos de valor en el montón de recolección de elementos no
utilizados. La conversión boxing es una conversión implícita de un tipo de valor en el tipo object o en cualquier
tipo de interfaz implementado por este tipo de valor. Al aplicar la conversión boxing a un tipo de valor se asigna
una instancia de objeto en el montón y se copia el valor en el nuevo objeto.
Considere la siguiente declaración de una variable de tipo de valor:
int i = 123;
El resultado de esta instrucción es crear una referencia de objeto o en la pila que hace referencia a un valor del
tipo int en el montón. Este valor es una copia del tipo de valor asignado a la variable i . La diferencia entre las
dos variables, i y o , se muestra en la imagen siguiente de la conversión boxing:
También es posible realizar la conversión boxing de manera explícita, tal como se muestra en el ejemplo
siguiente, pero esta nunca es necesaria:
int i = 123;
object o = (object)i; // explicit boxing
Ejemplo
Este ejemplo convierte una variable de entero i en un objeto o mediante la conversión boxing. A
continuación, el valor almacenado en la variable i se cambia de 123 a 456 . El ejemplo muestra que el tipo de
valor original y el objeto al que se ha aplicado la conversión boxing usan ubicaciones de memoria
independientes y, por consiguiente, pueden almacenar valores diferentes.
class TestBoxing
{
static void Main()
{
int i = 123;
Conversión unboxing
La conversión unboxing es una conversión explícita del tipo object en un tipo de valor o de un tipo de interfaz
en un tipo de valor que implementa la interfaz. Una operación de conversión unboxing consiste en lo siguiente:
Comprobar la instancia de objeto para asegurarse de que se trata de un valor de conversión boxing del
tipo de valor dado.
Copiar el valor de la instancia en la variable de tipo de valor.
Las siguientes instrucciones muestran las operaciones de conversión boxing y unboxing:
Para que la conversión unboxing de tipos de valor sea correcta en tiempo de ejecución, el elemento al que se
aplica debe ser una referencia a un objeto creado previamente mediante la conversión boxing de una instancia
de ese tipo de valor. Si se intenta aplicar la conversión unboxing a null , se producirá una excepción
NullReferenceException. Si se intenta aplicar la conversión unboxing a una referencia de un tipo de valor
incompatible, se producirá una excepción InvalidCastException.
Ejemplo
El ejemplo siguiente muestra un caso de conversión unboxing no válida y la excepción InvalidCastException
resultante. Si se utiliza try y catch , se muestra un mensaje de error cuando se produce el error.
class TestUnboxing
{
static void Main()
{
int i = 123;
object o = i; // implicit boxing
try
{
int j = (short)o; // attempt to unbox
System.Console.WriteLine("Unboxing OK.");
}
catch (System.InvalidCastException e)
{
System.Console.WriteLine("{0} Error: Incorrect unboxing.", e.Message);
}
}
}
int j = (short)o;
a:
int j = (int)o;
Vea también
Guía de programación de C#
Tipos de referencia
Tipos de valor
Procedimiento Convertir una matriz de bytes en un
valor int (Guía de programación de C#)
16/09/2021 • 2 minutes to read
En este ejemplo se muestra cómo usar la clase BitConverter para convertir una matriz de bytes en un valor int y
de nuevo en una matriz de bytes. Por ejemplo, es posible que tenga que realizar una conversión de bytes a un
tipo de datos integrado después de leer los bytes fuera de la red. Además del método ToInt32(Byte[], Int32) del
ejemplo, en la tabla siguiente se muestran los métodos de la clase BitConverter que sirven para convertir bytes
(de una matriz de bytes) en otros tipos integrados.
T IP O DEVUELTO M ÉTO DO
Ejemplos
En este ejemplo se inicializa una matriz de bytes, se invierte la matriz si la arquitectura de equipo es little-endian
(es decir, en primer lugar se almacena el byte menos significativo) y, después, se llama al método ToInt32(Byte[],
Int32) para convertir cuatro bytes de la matriz en int . El segundo argumento de ToInt32(Byte[], Int32)
especifica el índice de inicio de la matriz de bytes.
NOTE
El resultado puede cambiar en función de los modos endian de la arquitectura del equipo.
byte[] bytes = { 0, 0, 0, 25 };
En este ejemplo, el método GetBytes(Int32) de la clase BitConverter se llama para convertir int en una matriz
de bytes.
NOTE
El resultado puede cambiar en función de los modos endian de la arquitectura del equipo.
Consulte también
BitConverter
IsLittleEndian
Tipos
Procedimiento Convertir una cadena en un número
(Guía de programación de C#)
16/09/2021 • 5 minutes to read
Puede convertir string en un número si llama al método Parse o TryParse que se encuentra en tipos
numéricos ( int , long , double , etc.), o bien mediante los métodos de la clase System.Convert.
Resulta algo más eficaz y sencillo llamar a un método TryParse (por ejemplo, int.TryParse("11", out number) )
o un método Parse (por ejemplo, var number = int.Parse("11") ). El uso de un método Convert resulta más
práctico para objetos generales que implementan IConvertible.
Puede usar los métodos Parse o TryParse sobre el tipo numérico que espera que contenga la cadena, como el
tipo System.Int32. El método Convert.ToInt32 utiliza Parse internamente. El método Parse devuelve el número
convertido; el método TryParse devuelve un valor booleano que indica si la conversión se realizó
correctamente, y devuelve el número convertido en un parámetro out . Si el formato de la cadena no es válido,
Parse inicia una excepción, pero TryParse devuelve false . Cuando se llama a un método Parse , siempre
debe usar control de excepciones para detectar un FormatException cuando se produzca un error en la
operación de análisis.
try
{
int numVal = Int32.Parse("-105");
Console.WriteLine(numVal);
}
catch (FormatException e)
{
Console.WriteLine(e.Message);
}
// Output: -105
try
{
int m = Int32.Parse("abc");
}
catch (FormatException e)
{
Console.WriteLine(e.Message);
}
// Output: Input string was not in a correct format.
En el ejemplo siguiente se muestra un enfoque para analizar una cadena que se espera que incluya caracteres
numéricos iniciales (incluidos caracteres hexadecimales) y caracteres no numéricos finales. Asigna caracteres
válidos desde el principio de una cadena a una nueva cadena antes de llamar al método TryParse. Dado que las
cadenas que va a analizar contiene pocos caracteres, el ejemplo llama al método String.Concat para asignar
caracteres válidos a una nueva cadena. Para una cadena más larga, se puede usar la clase StringBuilder en su
lugar.
using System;
T IP O N UM ÉRIC O M ÉTO DO
decimal ToDecimal(String)
T IP O N UM ÉRIC O M ÉTO DO
float ToSingle(String)
double ToDouble(String)
short ToInt16(String)
int ToInt32(String)
long ToInt64(String)
ushort ToUInt16(String)
uint ToUInt32(String)
ulong ToUInt64(String)
En este ejemplo, se llama al método Convert.ToInt32(String) para convertir una cadena de entrada en un valor
int. El ejemplo detecta las dos excepciones más comunes que este método puede generar, FormatException y
OverflowException. Si se puede incrementar el número resultante sin exceder Int32.MaxValue, el ejemplo suma
1 al resultado y muestra la salida.
using System;
while (repeat)
{
Console.Write("Enter a number between −2,147,483,648 and +2,147,483,647 (inclusive): ");
Ejemplos
En este ejemplo se genera el valor hexadecimal de cada uno de los caracteres de string . Primero, analiza
string como una matriz de caracteres. Después, llama a ToInt32(Char) en cada carácter para obtener su valor
numérico. Finalmente, aplica formato al número como su representación hexadecimal en un elemento string .
En este ejemplo se analiza un elemento string de valores hexadecimales y genera el carácter correspondiente
a cada valor hexadecimal. Primero, llama al método Split(Char[]) para obtener cada valor hexadecimal como un
elemento string individual en una matriz. Después, llama a ToInt32(String, Int32) para convertir el valor
hexadecimal a un valor decimal representado como int. Muestra dos maneras distintas de obtener el carácter
correspondiente a ese código de carácter. En la primera técnica se usa ConvertFromUtf32(Int32), que devuelve
el carácter correspondiente al argumento de tipo entero como string . En la segunda técnica, int se convierte
de manera explícita en un elemento char.
string hexValues = "48 65 6C 6C 6F 20 57 6F 72 6C 64 21";
string[] hexValuesSplit = hexValues.Split(' ');
foreach (string hex in hexValuesSplit)
{
// Convert the number expressed in base-16 to an integer.
int value = Convert.ToInt32(hex, 16);
// Get the character corresponding to the integral value.
string stringValue = Char.ConvertFromUtf32(value);
char charValue = (char)value;
Console.WriteLine("hexadecimal value = {0}, int value = {1}, char value = {2} or {3}",
hex, value, stringValue, charValue);
}
/* Output:
hexadecimal value = 48, int value = 72, char value = H or H
hexadecimal value = 65, int value = 101, char value = e or e
hexadecimal value = 6C, int value = 108, char value = l or l
hexadecimal value = 6C, int value = 108, char value = l or l
hexadecimal value = 6F, int value = 111, char value = o or o
hexadecimal value = 20, int value = 32, char value = or
hexadecimal value = 57, int value = 87, char value = W or W
hexadecimal value = 6F, int value = 111, char value = o or o
hexadecimal value = 72, int value = 114, char value = r or r
hexadecimal value = 6C, int value = 108, char value = l or l
hexadecimal value = 64, int value = 100, char value = d or d
hexadecimal value = 21, int value = 33, char value = ! or !
*/
En este ejemplo se muestra otra manera de convertir un string hexadecimal en un entero mediante la llamada
al método Parse(String, NumberStyles).
En el siguiente ejemplo se muestra cómo convertir un string hexadecimal en un elemento float con la clase
System.BitConverter y el método UInt32.Parse.
// Output: 200.0056
En el ejemplo siguiente se muestra cómo convertir una matriz byte en una cadena hexadecimal mediante la
clase System.BitConverter.
byte[] vals = { 0x01, 0xAA, 0xB1, 0xDC, 0x10, 0xDD };
/*Output:
01-AA-B1-DC-10-DD
01AAB1DC10DD
*/
En el ejemplo siguiente se muestra cómo convertir una matriz byte en una cadena hexadecimal mediante una
llamada al método Convert.ToHexString introducido en .NET 5.0.
/*Output:
646F74636574
*/
Vea también
Cadenas con formato numérico estándar
Tipos
Determinación de si una cadena representa un valor numérico
Uso de tipo dinámico (Guía de programación de
C#)
16/09/2021 • 5 minutes to read
C# 4 introduce un nuevo tipo, dynamic . Se trata de un tipo estático, pero un objeto de tipo dynamic omite la
comprobación de tipos estáticos. En la mayoría de los casos, funciona como si tuviera el tipo object . En tiempo
de compilación, se supone que un elemento con tipo dynamic admite cualquier operación. Por consiguiente, no
tendrá que preocuparse de si el objeto obtiene su valor de una API de COM, de un lenguaje dinámico como
IronPython, del Document Object Model (DOM) HTML, de la reflexión o de otro lugar en el programa. Pero si el
código no es válido, los errores se detectan en tiempo de ejecución.
Por ejemplo, si la instancia de método exampleMethod1 del código siguiente solo tiene un parámetro, el
compilador reconoce que la primera llamada al método, ec.exampleMethod1(10, 4) , no es válida porque
contiene dos argumentos. La llamada genera un error del compilador. El compilador no comprueba la segunda
llamada al método, dynamic_ec.exampleMethod1(10, 4) , porque el tipo de dynamic_ec es dynamic . Por
consiguiente, no se notifica ningún error del compilador. Pero el error no pasa inadvertido indefinidamente. Se
detecta en tiempo de ejecución y genera una excepción en tiempo de ejecución.
class ExampleClass
{
public ExampleClass() { }
public ExampleClass(int v) { }
El rol del compilador en estos ejemplos consiste en empaquetar información sobre lo que cada instrucción se
propone para el objeto o la expresión con el tipo dynamic . En tiempo de ejecución, la información almacenada
se examina y cualquier instrucción que no sea válida genera una excepción en tiempo de ejecución.
El resultado de la mayoría de las operaciones dinámicas es dynamic . Por ejemplo, si se sitúa el puntero del
mouse sobre el uso de testSum en el ejemplo siguiente, IntelliSense muestra el tipo (local variable) dynamic
testSum .
dynamic d = 1;
var testSum = d + 3;
// Rest the mouse pointer over testSum in the following statement.
System.Console.WriteLine(testSum);
Conversiones
Las conversiones entre objetos dinámicos y de otro tipo son fáciles. Esto permite al desarrollador cambiar entre
el comportamiento dinámico y no dinámico.
Cualquier objeto se puede convertir implícitamente al tipo dinámico, tal y como se muestra en los ejemplos
siguientes.
dynamic d1 = 7;
dynamic d2 = "a string";
dynamic d3 = System.DateTime.Today;
dynamic d4 = System.Diagnostics.Process.GetProcesses();
En cambio, una conversión implícita se puede aplicar dinámicamente a cualquier expresión de tipo dynamic .
int i = d1;
string str = d2;
DateTime dt = d3;
System.Diagnostics.Process[] procs = d4;
// The following statement does not cause a compiler error, even though ec is not
// dynamic. A run-time exception is raised because the run-time type of d1 is int.
ec.exampleMethod2(d1);
// The following statement does cause a compiler error.
//ec.exampleMethod2(7);
Interoperabilidad COM
C# 4 incluye varias características que mejoran la experiencia de interoperar con API de COM tales como las API
de automatización de Office. Entre las mejoras se incluye el uso del tipo dynamic y de argumentos opcionales y
con nombre.
Muchos métodos COM permiten variaciones en los tipos de argumento y el tipo de valor devuelto mediante la
designación de los tipos como object . Esto requería una conversión explícita de los valores para coordinarlos
con variables fuertemente tipadas en C#. Si se compila con la opción EmbedInteropTypes (opciones del
compilador de C#) la introducción del tipo dynamic le permite tratar las repeticiones de object en las
signaturas de COM como si fueran de tipo dynamic y así evitar gran parte de la conversión. Por ejemplo, las
instrucciones siguientes comparan cómo se accede a una celda de una hoja de cálculo de Microsoft Office Excel
con el tipo dynamic y sin el tipo dynamic .
// After the introduction of dynamic, the access to the Value property and
// the conversion to Excel.Range are handled by the run-time COM binder.
excelApp.Cells[1, 1].Value = "Name";
Excel.Range range2010 = excelApp.Cells[1, 1];
Temas relacionados
T IT L E DESC RIP C IÓ N
Información general sobre Dynamic Language Runtime Ofrece información general sobre DLR, que es un entorno en
tiempo de ejecución que agrega un conjunto de servicios
para lenguajes dinámicos en Common Language Runtime
(CLR).
Tutorial: Crear y usar objetos dinámicos Ofrece instrucciones paso a paso para crear un objeto
dinámico personalizado y para crear un proyecto que acceda
a una biblioteca de IronPython .
T IT L E DESC RIP C IÓ N
Procedimiento para acceder a objetos de interoperabilidad Muestra cómo crear un proyecto que use argumentos
de Office mediante características de C# opcionales y con nombre, el tipo dynamic y otras mejoras
que simplifican el acceso a objetos de la API de Office.
Tutorial: Crear y usar objetos dinámicos (C# y Visual
Basic)
16/09/2021 • 12 minutes to read
Los objetos dinámicos exponen miembros como propiedades y métodos en tiempo de ejecución, en lugar de en
tiempo de compilación. Esto le permite crear objetos para trabajar con estructuras que no coinciden con un
formato o tipo estático. Por ejemplo, puede usar un objeto dinámico para hacer referencia a Document Object
Model (DOM) HTML, que puede contener cualquier combinación de atributos y elementos de marcado HTML
válidos. Dado que cada documento HTML es único, los miembros de un documento HTML específico se
determinan en tiempo de ejecución. Un método común para hacer referencia a un atributo de un elemento
HTML consiste en pasar el nombre del atributo al método GetProperty del elemento. Para hacer referencia al
atributo id del elemento HTML <div id="Div1"> , primero debe obtener una referencia al elemento <div> y,
después, usar divElement.GetProperty("id") . Si usa un objeto dinámico, puede hacer referencia al atributo id
como divElement.id .
Los objetos dinámicos también proporcionan un acceso cómodo a lenguajes dinámicos como IronPython e
IronRuby. Puede usar un objeto dinámico para hacer referencia a un script dinámico que se interpreta en tiempo
de ejecución.
Para hacer referencia a un objeto dinámico, use un enlace en tiempo de ejecución. En C#, el tipo de un objeto
enlazado en tiempo de ejecución se especifica como dynamic . En Visual Basic, el tipo de un objeto enlazado en
tiempo de ejecución se especifica como Object . Para obtener más información, vea dynamic y Enlace en tiempo
de compilación y en tiempo de ejecución.
Puede crear objetos dinámicos personalizados con las clases del espacio de nombres System.Dynamic. Por
ejemplo, puede crear un objeto ExpandoObject y especificar los miembros de ese objeto en tiempo de ejecución.
También puede crear su propio tipo que hereda la clase DynamicObject. Después, puede invalidar los miembros
de la clase DynamicObject para proporcionar funciones dinámicas en tiempo de ejecución.
Este artículo contiene dos tutoriales independientes:
Crear un objeto personalizado que expone dinámicamente el contenido de un archivo de texto como
propiedades de un objeto.
Crear un proyecto que usa una biblioteca IronPython .
Puede elegir una de estas dos opciones, o las dos, y, si opta por las dos, el orden no importa.
Prerrequisitos
Visual Studio 2019, versión 16.9 o posterior con la carga de trabajo Desarrollo de escritorio de .NET
instalada. El SDK de .NET 5.0 se instala automáticamente al seleccionar esta carga de trabajo.
NOTE
Es posible que el equipo muestre nombres o ubicaciones diferentes para algunos de los elementos de la interfaz de
usuario de Visual Studio en las siguientes instrucciones. La edición de Visual Studio que se tenga y la configuración que se
utilice determinan estos elementos. Para obtener más información, vea Personalizar el IDE.
En el segundo tutorial, instale IronPython para .NET. Vaya a su página de descarga para obtener la versión
más reciente.
using System.IO;
using System.Dynamic;
Imports System.IO
Imports System.Dynamic
8. El objeto dinámico personalizado usa una enumeración para determinar los criterios de búsqueda. Antes
de la instrucción de clase, agregue la siguiente definición de enumeración.
public enum StringSearchOption
{
StartsWith,
Contains,
EndsWith
}
9. Actualice la instrucción de clase para heredar la clase DynamicObject , como se muestra en el ejemplo de
código siguiente.
10. Agregue el código siguiente a la clase ReadOnlyFile para definir un campo privado para la ruta de acceso
y un constructor para la clase ReadOnlyFile .
// Store the path to the file and the initial line count value.
private string p_filePath;
// Public constructor. Verify that file exists and store the path in
// the private variable.
public ReadOnlyFile(string filePath)
{
if (!File.Exists(filePath))
{
throw new Exception("File path does not exist.");
}
p_filePath = filePath;
}
' Store the path to the file and the initial line count value.
Private p_filePath As String
' Public constructor. Verify that file exists and store the path in
' the private variable.
Public Sub New(ByVal filePath As String)
If Not File.Exists(filePath) Then
Throw New Exception("File path does not exist.")
End If
p_filePath = filePath
End Sub
try
{
sr = new StreamReader(p_filePath);
while (!sr.EndOfStream)
{
line = sr.ReadLine();
switch (StringSearchOption)
{
case StringSearchOption.StartsWith:
if (testLine.StartsWith(propertyName.ToUpper())) { results.Add(line); }
break;
case StringSearchOption.Contains:
if (testLine.Contains(propertyName.ToUpper())) { results.Add(line); }
break;
case StringSearchOption.EndsWith:
if (testLine.EndsWith(propertyName.ToUpper())) { results.Add(line); }
break;
}
}
}
catch
{
// Trap any exception that occurs in reading the file and return null.
results = null;
}
finally
{
if (sr != null) {sr.Close();}
}
return results;
}
Public Function GetPropertyValue(ByVal propertyName As String,
Optional ByVal StringSearchOption As StringSearchOption =
StringSearchOption.StartsWith,
Optional ByVal trimSpaces As Boolean = True) As List(Of String)
Try
sr = New StreamReader(p_filePath)
Return results
End Function
12. Después del método GetPropertyValue , agregue el código siguiente para invalidar el método
TryGetMember de la clase DynamicObject. Se llama al método TryGetMember cuando se solicita un
miembro de una clase dinámica y no se especifican argumentos. El argumento binder contiene
información sobre el miembro al que se hace referencia y el argumento result hace referencia al
resultado devuelto para el miembro especificado. El método TryGetMember devuelve un valor booleano
que devuelve true si el miembro solicitado existe. En caso contrario, devuelve false .
// Implement the TryGetMember method of the DynamicObject class for dynamic member calls.
public override bool TryGetMember(GetMemberBinder binder,
out object result)
{
result = GetPropertyValue(binder.Name);
return result == null ? false : true;
}
' Implement the TryGetMember method of the DynamicObject class for dynamic member calls.
Public Overrides Function TryGetMember(ByVal binder As GetMemberBinder,
ByRef result As Object) As Boolean
result = GetPropertyValue(binder.Name)
Return If(result Is Nothing, False, True)
End Function
13. Después del método TryGetMember , agregue el código siguiente para invalidar el método
TryInvokeMember de la clase DynamicObject. Se llama al método TryInvokeMember cuando se solicita
un miembro de una clase dinámica con argumentos. El argumento binder contiene información sobre el
miembro al que se hace referencia y el argumento result hace referencia al resultado devuelto para el
miembro especificado. El argumento args contiene una matriz de los argumentos que se pasan al
miembro. El método TryInvokeMember devuelve un valor booleano que devuelve true si el miembro
solicitado existe. En caso contrario, devuelve false .
La versión personalizada del método TryInvokeMember espera que el primer argumento sea un valor del
enumerador StringSearchOption que se ha definido en un paso anterior. El método TryInvokeMember
espera que el segundo argumento sea un valor booleano. Si uno o ambos argumentos son valores
válidos, se pasan al método GetPropertyValue para recuperar los resultados.
try
{
if (args.Length > 0) { StringSearchOption = (StringSearchOption)args[0]; }
}
catch
{
throw new ArgumentException("StringSearchOption argument must be a StringSearchOption enum
value.");
}
try
{
if (args.Length > 1) { trimSpaces = (bool)args[1]; }
}
catch
{
throw new ArgumentException("trimSpaces argument must be a Boolean value.");
}
Try
If args.Length > 0 Then StringSearchOption = CType(args(0), StringSearchOption)
Catch
Throw New ArgumentException("StringSearchOption argument must be a StringSearchOption enum
value.")
End Try
Try
If args.Length > 1 Then trimSpaces = CType(args(1), Boolean)
Catch
Throw New ArgumentException("trimSpaces argument must be a Boolean value.")
End Try
using System.Linq;
using Microsoft.Scripting.Hosting;
using IronPython.Hosting;
Imports Microsoft.Scripting.Hosting
Imports IronPython.Hosting
Imports System.Linq
8. En el método Main, agregue el código siguiente para crear un objeto
Microsoft.Scripting.Hosting.ScriptRuntime que hospede las bibliotecas de IronPython. El objeto
ScriptRuntime carga el módulo de biblioteca de IronPython random.py.
9. Una vez que el código haya cargado el módulo random.py, agregue el código siguiente para crear una
matriz de enteros. La matriz se pasa al método shuffle del módulo random.py, que ordena
aleatoriamente los valores de la matriz.
El lenguaje C# está diseñado para que las versiones entre clases base y derivadas de diferentes bibliotecas
puedan evolucionar y mantener la compatibilidad con versiones anteriores. Esto significa, por ejemplo, que la
introducción de un nuevo miembro en una clase base con el mismo nombre que un miembro de una clase
derivada es totalmente compatible con C# y no lleva a un comportamiento inesperado. Además, implica que
una clase debe declarar explícitamente si un método está pensado para reemplazar un método heredado o si se
trata de un nuevo método que oculta un método heredado de nombre similar.
En C#, las clases derivadas pueden contener métodos con el mismo nombre que los métodos de clase base.
Si el método de la clase derivada no va precedido por las palabras clave new u override, el compilador
emite una advertencia y el método se comporta como si la palabra clave new estuviese presente.
Si el método de la clase derivada va precedido de la palabra clave new , el método se define como
independiente del método de la clase base.
Si el método de la clase derivada va precedido de la palabra clave override , los objetos de la clase
derivada llamarán a ese método y no al método de la clase base.
Para aplicar la palabra clave override al método de la clase derivada, se debe definir el método de clase
base como virtual.
El método de clase base puede llamarse desde dentro de la clase derivada mediante la palabra clave
base .
Las palabras clave override , virtual y new también pueden aplicarse a propiedades, indexadores y
eventos.
De forma predeterminada, los métodos de C# no son virtuales. Si se declara un método como virtual, toda clase
que hereda el método puede implementar su propia versión Para que un método sea virtual, se usa el
modificador virtual en la declaración del método de la clase base. La clase derivada puede luego reemplazar
el método base virtual mediante la palabra clave override u ocultar el método virtual en la clase base mediante
la palabra clave new . Si no se especifican las palabras clave override o new , el compilador emite una
advertencia y el método de la clase derivada oculta el método de la clase base.
Para demostrar esto en la práctica, supongamos por un momento que la compañía A ha creado una clase
denominada GraphicsClass , que su programa usa. La siguiente es GraphicsClass :
class GraphicsClass
{
public virtual void DrawLine() { }
public virtual void DrawPoint() { }
}
Su compañía usa esta clase y usted la usa para derivar su propia clase, agregando un nuevo método:
class YourDerivedGraphicsClass : GraphicsClass
{
public void DrawRectangle() { }
}
La aplicación se usa sin problemas, hasta que la compañía A lanza una nueva versión de GraphicsClass , que es
similar al código siguiente:
class GraphicsClass
{
public virtual void DrawLine() { }
public virtual void DrawPoint() { }
public virtual void DrawRectangle() { }
}
Si quiere que su método reemplace al nuevo método de clase base, use la palabra clave override :
La palabra clave override se asegura de que los objetos derivados de YourDerivedGraphicsClass usen la
versión de la clase derivada de DrawRectangle . Los objetos derivados de YourDerivedGraphicsClass todavía
pueden acceder a la versión de clase base DrawRectangle mediante la palabra clave base:
base.DrawRectangle();
Si no quiere que el método reemplace al nuevo método de clase base, se aplican las consideraciones siguientes.
Para evitar la confusión entre los dos métodos, puede cambiarle el nombre a su método. Esto puede ser un
proceso lento y propenso a errores y no resultar práctico en algunos casos. Pero si el proyecto es relativamente
pequeño, puede usar opciones de refactorización de Visual Studio para cambiar el nombre del método. Para
obtener más información, vea Refactoring Classes and Types (Class Designer) (Refactorización de clases y tipos
[Diseñador de clases]).
También puede evitar la advertencia mediante la palabra clave new en la definición de clase derivada:
Con la palabra clave new se indica al compilador que su definición oculta la definición contenida en la clase
base. Éste es el comportamiento predeterminado.
Selección de método y reemplazo
Cuando se llama a un método en una clase, el compilador de C# selecciona el mejor método para llamar si hay
más de uno compatible con la llamada, como cuando hay dos métodos con el mismo nombre y parámetros que
son compatibles con el parámetro pasado. Los métodos siguientes serían compatibles:
Cuando se llama a DoWork en una instancia de Derived , el compilador de C# intentará en primer lugar que la
llamada sea compatible con las versiones de DoWork declaradas originalmente en Derived . Los métodos de
reemplazo no se consideran como declarados en una clase, son nuevas implementaciones de un método que se
declara en una clase base. Solo si el compilador de C# no puede hacer coincidir la llamada de método con un
método original en Derived , intentará hacer coincidir la llamada con un método reemplazado con el mismo
nombre y parámetros compatibles. Por ejemplo:
int val = 5;
Derived d = new Derived();
d.DoWork(val); // Calls DoWork(double).
Dado que la variable val se puede convertir implícitamente en un valor doble, el compilador de C# llama a
DoWork(double) en lugar de a DoWork(int) . Hay dos maneras de evitarlo. En primer lugar, evite declarar nuevos
métodos con el mismo nombre que los métodos virtuales. En segundo lugar, puede indicar al compilador de C#
que llame al método virtual haciendo que busque la lista de métodos de clase base mediante la conversión de la
instancia de Derived a Base . Como el método es virtual, se llamará a la implementación de DoWork(int) en
Derived . Por ejemplo:
Para obtener otros ejemplos de new y override , vea Saber cuándo utilizar las palabras clave Override y New
(Guía de programación de C#).
Vea también
Guía de programación de C#
Clases, estructuras y registros
Métodos
Herencia
Saber cuándo utilizar las palabras clave Override y
New (Guía de programación de C#)
16/09/2021 • 11 minutes to read
En C#, un método de una clase derivada puede tener el mismo nombre que un método de la clase base. Se
puede especificar cómo interactúan los métodos mediante las palabras clave new y override. El modificador
override extiende el método de clase base virtual y el modificador new oculta un método de clase base
accesible. En los ejemplos de este tema se ilustra la diferencia.
En una aplicación de consola, declare las dos clases siguientes, BaseClass y DerivedClass . DerivedClass hereda
de BaseClass .
class BaseClass
{
public void Method1()
{
Console.WriteLine("Base - Method1");
}
}
bc.Method1();
dc.Method1();
dc.Method2();
bcdc.Method1();
}
// Output:
// Base - Method1
// Base - Method1
// Derived - Method2
// Base - Method1
}
Después, agregue el método Method2 siguiente a BaseClass . La firma de este método coincide con la firma del
método Method2 de DerivedClass .
Dado que BaseClass ahora tiene un método Method2 , se puede agregar una segunda instrucción de llamada
para las variables de BaseClass``bc y bcdc , como se muestra en el código siguiente.
bc.Method1();
bc.Method2();
dc.Method1();
dc.Method2();
bcdc.Method1();
bcdc.Method2();
Al compilar el proyecto, verá que la adición del método Method2 de BaseClass genera una advertencia. La
advertencia indica que el método Method2 de DerivedClass oculta el método Method2 de BaseClass . Se
recomienda usar la palabra clave new en la definición de Method2 si se pretende provocar ese resultado. Como
alternativa, se puede cambiar el nombre de uno de los métodos Method2 para resolver la advertencia, pero eso
no siempre resulta práctico.
Antes de agregar new , ejecute el programa para ver el resultado producido por las instrucciones adicionales
que realizan la llamada. Se muestran los resultados siguientes.
// Output:
// Base - Method1
// Base - Method2
// Base - Method1
// Derived - Method2
// Base - Method1
// Base - Method2
La palabra clave new conserva las relaciones que generan ese resultado, pero se suprime la advertencia. Las
variables de tipo BaseClass siguen teniendo acceso a los miembros de BaseClass y la variable de tipo
DerivedClass sigue teniendo acceso a los miembros de DerivedClass en primer lugar y, después, tiene en
cuenta los miembros heredados de BaseClass .
Para suprimir la advertencia, agregue el modificador new a la definición de Method2 en DerivedClass , como se
muestra en el código siguiente. Se puede agregar el modificador antes o después de public .
Vuelva a ejecutar el programa para comprobar que el resultado no ha cambiado. Compruebe también que ya no
aparece la advertencia. Mediante el uso de new , afirma que es consciente de que el miembro que modifica
oculta un miembro heredado de la clase base. Para más información sobre la ocultación de nombres a través de
la herencia, vea new (Modificador, Referencia de C#).
Para contrastar este comportamiento con los efectos de usar override , agregue el método siguiente a
DerivedClass . Se puede agregar el modificador override antes o después de public .
Vuelva a ejecutar el proyecto. Observe especialmente las dos últimas líneas del resultado siguiente.
// Output:
// Base - Method1
// Base - Method2
// Derived - Method1
// Derived - Method2
// Derived - Method1
// Base - Method2
El uso del modificador override permite que bcdc tenga acceso al método Method1 que se define en
DerivedClass . Normalmente, es el comportamiento deseado en jerarquías de herencia. La intención es que los
objetos que tienen valores que se crean a partir de la clase derivada usen los métodos que se definen en la clase
derivada. Ese comportamiento se consigue mediante el uso de override para extender el método de clase base.
El código siguiente contiene el ejemplo completo.
using System;
using System.Text;
namespace OverrideAndNew
{
class Program
{
static void Main(string[] args)
{
BaseClass bc = new BaseClass();
DerivedClass dc = new DerivedClass();
BaseClass bcdc = new DerivedClass();
// The following two calls do what you would expect. They call
// the methods that are defined in BaseClass.
bc.Method1();
bc.Method2();
// Output:
// Base - Method1
// Base - Method2
// The following two calls do what you would expect. They call
// the methods that are defined in DerivedClass.
dc.Method1();
dc.Method2();
// Output:
// Derived - Method1
// Derived - Method2
class BaseClass
{
public virtual void Method1()
{
Console.WriteLine("Base - Method1");
}
// Define the base class, Car. The class defines two methods,
// DescribeCar and ShowDetails. DescribeCar calls ShowDetails, and each derived
// class also defines a ShowDetails method. The example tests which version of
// ShowDetails is selected, the base class method or the derived class method.
class Car
{
public void DescribeCar()
{
System.Console.WriteLine("Four wheels and an engine.");
ShowDetails();
}
El ejemplo comprueba la versión de ShowDetails que se llama. El siguiente método, TestCars1 , declara una
instancia de cada clase y, después, llama a DescribeCar en cada instancia.
public static void TestCars1()
{
System.Console.WriteLine("\nTestCars1");
System.Console.WriteLine("----------");
// Notice the output from this test case. The new modifier is
// used in the definition of ShowDetails in the ConvertibleCar
// class.
TestCars1 genera el siguiente resultado. Observe especialmente los resultados de car2 , que probablemente
no son los que se esperaban. El tipo de objeto es ConvertibleCar , pero DescribeCar no tiene acceso a la versión
de ShowDetails que se define en la clase ConvertibleCar porque ese método se declara con el modificador
new , no con el modificador override . Como resultado, un objeto ConvertibleCar muestra la misma
descripción que un objeto Car . Compare los resultados de car3 , que es un objeto Minivan . En este caso, el
método ShowDetails que se declara en la clase Minivan invalida el método ShowDetails que se declara en la
clase Car , y en la descripción que se muestra se describe una furgoneta.
// TestCars1
// ----------
// Four wheels and an engine.
// Standard transportation.
// ----------
// Four wheels and an engine.
// Standard transportation.
// ----------
// Four wheels and an engine.
// Carries seven people.
// ----------
TestCars2 crea una lista de objetos que tienen el tipo Car . Se crean instancias de los valores de los objetos
desde las clases Car , ConvertibleCar y Minivan . DescribeCar se llama en cada elemento de la lista. En el
código siguiente se muestra la definición de TestCars2 .
// TestCars2
// ----------
// Four wheels and an engine.
// Standard transportation.
// ----------
// Four wheels and an engine.
// Standard transportation.
// ----------
// Four wheels and an engine.
// Carries seven people.
// ----------
Los métodos TestCars3 y TestCars4 completan el ejemplo. Estos métodos llaman directamente a ShowDetails ,
primero desde los objetos declarados con el tipo ConvertibleCar y Minivan ( TestCars3 ), y después desde los
objetos declarados con el tipo Car ( TestCars4 ). En el código siguiente se definen estos dos métodos.
Los métodos generan el siguiente resultado, que se corresponde a los resultados del primer ejemplo de este
tema.
// TestCars3
// ----------
// A roof that opens up.
// Carries seven people.
// TestCars4
// ----------
// Standard transportation.
// Carries seven people.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq;
using System.Text;
namespace OverrideAndNew2
{
class Program
{
static void Main(string[] args)
{
// Declare objects of the derived classes and test which version
// of ShowDetails is run, base or derived.
TestCars1();
// Notice the output from this test case. The new modifier is
// used in the definition of ShowDetails in the ConvertibleCar
// class.
ConvertibleCar car2 = new ConvertibleCar();
car2.DescribeCar();
System.Console.WriteLine("----------");
// Define the base class, Car. The class defines two virtual methods,
// DescribeCar and ShowDetails. DescribeCar calls ShowDetails, and each derived
// class also defines a ShowDetails method. The example tests which version of
// ShowDetails is used, the base class method or the derived class method.
class Car
{
public virtual void DescribeCar()
{
System.Console.WriteLine("Four wheels and an engine.");
ShowDetails();
}
Vea también
Guía de programación de C#
Clases, estructuras y registros
Control de versiones con las palabras clave Override y New
base
abstract
Procedimiento para invalidar el método ToString
(Guía de programación de C#)
16/09/2021 • 2 minutes to read
Cada clase o struct de C# hereda implícitamente la clase Object. Por consiguiente, cada objeto de C# obtiene el
método ToString, que devuelve una representación de cadena de ese objeto. Por ejemplo, todas las variables de
tipo int tienen un método ToString , que las habilita para devolver su contenido como cadena:
int x = 42;
string strx = x.ToString();
Console.WriteLine(strx);
// Output:
// 42
Cuando cree una clase o struct personalizados, debe reemplazar el método ToString para proporcionar
información sobre el tipo al código de cliente.
Para obtener información sobre cómo usar cadenas de formato y otros tipos de formato personalizado con el
método ToString , vea Aplicar formato a tipos.
IMPORTANT
Cuando decida qué información va a proporcionar a través de este método, considere si la clase o struct se va a usar
alguna vez en código que no sea de confianza. Asegúrese de que no proporciona información que pueda ser aprovechada
por código malintencionado.
class Person
{
public string Name { get; set; }
public int Age { get; set; }
Se puede probar el método ToString tal y como se muestra en el siguiente ejemplo de código:
Person person = new Person { Name = "John", Age = 12 };
Console.WriteLine(person);
// Output:
// Person: John 12
Vea también
IFormattable
Guía de programación de C#
Clases, estructuras y registros
Cadenas
string
override
virtual
Aplicación de formato a tipos
Miembros (Guía de programación de C#)
16/09/2021 • 2 minutes to read
Las clases y structs tienen miembros que representan sus datos y comportamiento. Los miembros de una clase
incluyen todos los miembros declarados en la clase, junto con todos los miembros (excepto constructores y
finalizadores) declarados en todas las clases de su jerarquía de herencia. Los miembros privados de clases base
se heredan en las clases derivadas, pero estas no pueden tener acceso a ellos.
En la tabla siguiente se enumeran los tipos de miembros que puede contener una clase o struct:
Métodos Los métodos definen las acciones que una clase puede
realizar. Los métodos pueden aceptar parámetros que
proporcionan datos de entrada y devolver datos de salida a
través de parámetros. Los métodos también pueden
devolver un valor directamente, sin usar ningún parámetro.
Tipos anidados Los tipos anidados son tipos declarados dentro de otro tipo.
Los tipos anidados se usan a menudo para describir objetos
utilizados únicamente por los tipos que los contienen.
Vea también
Guía de programación de C#
Clases
Clases y miembros de clase abstractos y sellados
(Guía de programación de C#)
16/09/2021 • 2 minutes to read
La palabra clave abstract permite crear clases y miembros class que están incompletos y se deben implementar
en una clase derivada.
La palabra clave sealed permite impedir la herencia de una clase o de ciertos miembros de clase marcados
previamente como virtual.
No se pueden crear instancias de una clase abstracta. El propósito de una clase abstracta es proporcionar una
definición común de una clase base que múltiples clases derivadas pueden compartir. Por ejemplo, una
biblioteca de clases puede definir una clase abstracta que se utiliza como parámetro para muchas de sus
funciones y solicitar a los programadores que utilizan esa biblioteca que proporcionen su propia
implementación de la clase mediante la creación de una clase derivada.
Las clases abstractas también pueden definir métodos abstractos. Esto se consigue agregando la palabra clave
abstract antes del tipo de valor que devuelve el método. Por ejemplo:
Los métodos abstractos no tienen ninguna implementación, de modo que la definición de método va seguida
por un punto y coma en lugar de un bloque de método normal. Las clases derivadas de la clase abstracta deben
implementar todos los métodos abstractos. Cuando una clase abstracta hereda un método virtual de una clase
base, la clase abstracta puede reemplazar el método virtual con un método abstracto. Por ejemplo:
// compile with: -target:library
public class D
{
public virtual void DoWork(int i)
{
// Original implementation.
}
}
public class F : E
{
public override void DoWork(int i)
{
// New implementation.
}
}
Si un método virtual se declara como abstract , sigue siendo virtual para cualquier clase que herede de la
clase abstracta. Una clase que hereda un método abstracto no puede tener acceso a la implementación original
del método: en el ejemplo anterior, DoWork en la clase F no puede llamar a DoWork en la clase D. De esta
manera, una clase abstracta puede exigir a las clases derivadas que proporcionen nuevas implementaciones de
método para los métodos virtuales.
Una clase sellada no se puede utilizar como clase base. Por esta razón, tampoco puede ser una clase abstracta.
Las clases selladas evitan la derivación. Puesto que nunca se pueden utilizar como clase base, algunas
optimizaciones en tiempo de ejecución pueden hacer que sea un poco más rápido llamar a miembros de clase
sellada.
Un método, indizador, propiedad o evento de una clase derivada que reemplaza a un miembro virtual de la clase
base puede declarar ese miembro como sellado. Esto niega el aspecto virtual del miembro para cualquier clase
derivada adicional. Esto se logra colocando la palabra clave sealed antes de la palabra clave override en la
declaración del miembro de clase. Por ejemplo:
public class D : C
{
public sealed override void DoWork() { }
}
Consulte también
Guía de programación de C#
Clases, estructuras y registros
Herencia
Métodos
Campos
Procedimiento para definir propiedades abstractas
Clases estáticas y sus miembros (Guía de
programación de C#)
16/09/2021 • 6 minutes to read
Una clase estática es básicamente lo mismo que una clase no estática, con la diferencia de que no se pueden
crear instancias de una clase estática. En otras palabras, no puede usar el operador new para crear una variable
del tipo de clase. Dado que no hay ninguna variable de instancia, para tener acceso a los miembros de una clase
estática, debe usar el nombre de la clase. Por ejemplo, si tiene una clase estática denominada UtilityClass que
tiene un método estático público denominado MethodA , llame al método tal como se muestra en el ejemplo
siguiente:
UtilityClass.MethodA();
Es posible usar una clase estática como un contenedor adecuado para conjuntos de métodos que solo funcionan
en parámetros de entrada y que no tienen que obtener ni establecer campos de instancias internas. Por ejemplo,
en la biblioteca de clases .NET, la clase estática System.Math contiene métodos que realizan operaciones
matemáticas, sin ningún requisito para almacenar o recuperar datos que sean únicos de una instancia concreta
de la clase Math. Es decir, para aplicar los miembros de la clase, debe especificar el nombre de clase y el nombre
de método, como se muestra en el ejemplo siguiente.
// Output:
// 3.14
// -4
// 3
Como sucede con todos los tipos de clase, el entorno de ejecución de .NET carga la información de tipo para una
clase estática cuando se carga el programa que hace referencia a la clase. El programa no puede especificar
exactamente cuándo se carga la clase, pero existe la garantía de que se cargará, de que sus campos se
inicializarán y de que se llamará a su constructor estático antes de que se haga referencia a la clase por primera
vez en el programa. Solo se llama una vez a un constructor estático, y una clase estática permanece en memoria
durante la vigencia del dominio de aplicación en el que reside el programa.
NOTE
Para crear una clase no estática que solo permita la creación de una instancia de sí misma, vea Implementing Singleton in
C# (Implementar un singleton en C#).
Ejemplo
A continuación se muestra un ejemplo de una clase estática que contiene dos métodos que convierten la
temperatura de grados Celsius a grados Fahrenheit y viceversa:
return fahrenheit;
}
return celsius;
}
}
class TestTemperatureConverter
{
static void Main()
{
Console.WriteLine("Please select the convertor direction");
Console.WriteLine("1. From Celsius to Fahrenheit.");
Console.WriteLine("2. From Fahrenheit to Celsius.");
Console.Write(":");
switch (selection)
{
case "1":
Console.Write("Please enter the Celsius temperature: ");
F = TemperatureConverter.CelsiusToFahrenheit(Console.ReadLine());
Console.WriteLine("Temperature in Fahrenheit: {0:F2}", F);
break;
case "2":
Console.Write("Please enter the Fahrenheit temperature: ");
C = TemperatureConverter.FahrenheitToCelsius(Console.ReadLine());
C = TemperatureConverter.FahrenheitToCelsius(Console.ReadLine());
Console.WriteLine("Temperature in Celsius: {0:F2}", C);
break;
default:
Console.WriteLine("Please select a convertor.");
break;
}
Miembros estáticos
Una clase no estática puede contener métodos, campos, propiedades o eventos estáticos. El miembro estático es
invocable en una clase, incluso si no se ha creado ninguna instancia de la clase. Siempre se tiene acceso al
miembro estático con el nombre de clase, no con el nombre de instancia. Solo existe una copia de un miembro
estático, independientemente del número de instancias de la clase que se creen. Los métodos y las propiedades
estáticos no pueden acceder a campos y eventos no estáticos en su tipo contenedor, ni tampoco a una variable
de instancia de un objeto a menos que se pase explícitamente en un parámetro de método.
Es más habitual declarar una clase no estática con algunos miembros estáticos que declarar toda una clase
como estática. Dos usos habituales de los campos estáticos son llevar la cuenta del número de objetos de los
que se han creado instancias y almacenar un valor que se debe compartir entre todas las instancias.
Los métodos estáticos se pueden sobrecargar pero no invalidar, ya que pertenecen a la clase y no a una
instancia de la clase.
Aunque un campo no se puede declarar como static const , el campo const es básicamente estático en su
comportamiento. Pertenece al tipo, no a las instancias del tipo. Por lo tanto, se puede acceder a campos const
mediante la misma notación ClassName.MemberName que se usa para los campos estáticos. No se requiere
ninguna instancia de objeto.
C# no admite variables locales estáticas (es decir, variables que se declaran en el ámbito del método).
Para declarar miembros de clases estáticas, use la palabra clave static antes del tipo de valor devuelto del
miembro, como se muestra en el ejemplo siguiente:
public class Automobile
{
public static int NumberOfWheels = 4;
Los miembros estáticos se inicializan antes de que se obtenga acceso por primera vez al miembro estático y
antes de que se llame al constructor estático, en caso de haberlo. Para tener acceso a un miembro de clase
estática, use el nombre de la clase en lugar de un nombre de variable para especificar la ubicación del miembro,
como se muestra en el ejemplo siguiente:
Automobile.Drive();
int i = Automobile.NumberOfWheels;
Si la clase contiene campos estáticos, proporcione un constructor estático que los inicialice al cargar la clase.
Una llamada a un método estático genera una instrucción de llamada en Lenguaje Intermedio de Microsoft
(MSIL), mientras que una llamada a un método de instancia genera una instrucción callvirt , que además
busca referencias a un objeto NULL, pero la mayoría de las veces la diferencia de rendimiento entre las dos no
es significativo.
Vea también
Guía de programación de C#
static
Clases
class
Constructores estáticos
Constructores de instancias
Modificadores de acceso (Guía de programación de
C#)
16/09/2021 • 4 minutes to read
Todos los tipos y miembros de tipo tienen un nivel de accesibilidad. El nivel de accesibilidad controla si se
pueden usar desde otro código del ensamblado u otros ensamblados. Use los modificadores de acceso
siguientes para especificar la accesibilidad de un tipo o miembro cuando lo declare:
public: Puede obtener acceso al tipo o miembro cualquier otro código del mismo ensamblado o de otro
ensamblado que haga referencia a éste.
private: solamente el código de la misma class o struct puede acceder al tipo o miembro.
protected: solamente el código de la misma class , o bien de una class derivada de esa class , puede
acceder al tipo o miembro.
internal: Puede obtener acceso al tipo o miembro cualquier código del mismo ensamblado, pero no de un
ensamblado distinto.
protected internal: cualquier código del ensamblado en el que se ha declarado, o desde una class derivada
de otro ensamblado, puede acceder al tipo o miembro.
private protected: el código de la misma class , o de un tipo derivado de esa class , puede acceder al tipo o
miembro solo dentro de su ensamblado de declaración.
Tabla de resumen
UB IC A C IÓ N
DEL A UTO R
DE L A PROTECTED PRIVATE
L L A M A DA PUBLIC INTERNAL PROTECTED INTERNAL PROTECTED PRIVATE
Dentro de la ️
✔ ️
✔ ️
✔ ️
✔ ️
✔ ️
✔
clase
Clase ️
✔ ️
✔ ️
✔ ️
✔ ️
✔ ❌
derivada
(mismo
ensamblado)
Clase no ️
✔ ️
✔ ❌ ️
✔ ❌ ❌
derivada
(mismo
ensamblado)
Clase ️
✔ ️
✔ ️
✔ ❌ ❌ ❌
derivada (otro
ensamblado)
Clase no ️
✔ ❌ ❌ ❌ ❌ ❌
derivada (otro
ensamblado)
En los ejemplos siguientes se muestra cómo especificar modificadores de acceso en un tipo y miembro:
public class Bicycle
{
public void Pedal() { }
}
No todos los modificadores de acceso son válidos para todos los tipos o miembros de todos los contextos. En
algunos casos, la accesibilidad de un miembro de tipo está restringida por la accesibilidad de su tipo contenedor.
Normalmente, la accesibilidad de un miembro no es mayor que la del tipo que lo contiene. Pero un miembro
public de una clase interna podría ser accesible desde fuera del ensamblado si el miembro implementa los
métodos de interfaz o invalida los métodos virtuales definidos en una clase base pública.
El tipo de cualquier miembro que sea un campo, propiedad o evento debe ser al menos tan accesible como el
propio miembro. Del mismo modo, el tipo devuelto y los tipos de parámetro de cualquier método, indizador o
delegado deben ser al menos tan accesibles como el propio miembro. Por ejemplo, no puede tener un método
public M que devuelva una clase C a menos que C también sea public . Del mismo modo, no puede tener
una propiedad protected de tipo A si A se declara como private .
Los operadores definidos por el usuario siempre se deben declarar como public y static . Para obtener más
información, vea Sobrecarga de operadores.
Los finalizadores no pueden tener modificadores de accesibilidad.
Para establecer el nivel de acceso de un miembro de class , record o struct , agregue la palabra clave
adecuada a la declaración de miembro, como se muestra en el ejemplo siguiente.
// public class:
public class Tricycle
{
// protected method:
protected void Pedal() { }
// private field:
private int wheels = 3;
Otros tipos
Las interfaces declaradas directamente en un espacio de nombres pueden ser public o internal y, al igual que
las clases y las estructuras, su valor predeterminado es el acceso internal . Los miembros de interfaz son
public de manera predeterminada porque el propósito de una interfaz es permitir que otros tipos accedan a
una clase o estructura. Las declaraciones de miembros de interfaz pueden incluir cualquier modificador de
acceso. Esto es muy útil para que los métodos estáticos proporcionen implementaciones comunes necesarias
para todos los implementadores de una clase.
Los miembros de enumeración siempre son public y no se les puede aplicar ningún modificador de acceso.
Los delegados se comportan como las clases y las estructuras. De forma predeterminada, tienen acceso
internal cuando se declaran directamente en un espacio de nombres y acceso private cuando están
anidados.
Vea también
Guía de programación de C#
Clases, estructuras y registros
Interfaces
private
public
internal
protected
protected internal
private protected
class
struct
interface
Campos (Guía de programación de C#)
16/09/2021 • 4 minutes to read
Un campo es una variable de cualquier tipo que se declara directamente en una clase o struct. Los campos son
miembros de su tipo contenedor.
Una clase o struct puede tener campos de instancia, campos estáticos o ambos. Los campos de instancia son
específicos de una instancia de un tipo. Si tiene una clase T, con un campo de instancia F, puede crear dos
objetos de tipo T y modificar el valor de F en cada objeto sin afectar el valor del otro objeto. Por el contrario, un
campo estático pertenece a la propia clase y se comparte entre todas las instancias de esa clase. Solo puede
tener acceso al campo estático mediante el nombre de clase. Si obtiene acceso al campo estático mediante un
nombre de instancia, obtendrá el error en tiempo de compilación CS0176.
Por lo general, solo se deben usar campos para las variables que tienen accesibilidad privada o protegida. Los
datos que la clase expone al código de cliente deben proporcionarse a través de métodos, propiedades e
indizadores. Mediante estas construcciones para el acceso indirecto a los campos internos, se puede proteger de
los valores de entrada no válidos. Un campo privado que almacena los datos expuestos por una propiedad
pública se denomina memoria auxiliar o campo de respaldo.
Los campos almacenan habitualmente los datos que deben ser accesibles para más de un método de clase y
que deben almacenarse durante más tiempo de lo que dura un único método. Por ejemplo, es posible que una
clase que representa una fecha de calendario tenga tres campos enteros: uno para el mes, otro para el día y otro
para el año. Las variables que no se usan fuera del ámbito de un único método se deben declarar como
variables locales dentro del campo del método.
Los campos se declaran en el bloque de clase especificando el nivel de acceso del campo, seguido por el tipo del
campo, seguido por el nombre del campo. Por ejemplo:
public class CalendarEntry
{
Para obtener acceso a un campo en un objeto, agregue un punto después del nombre de objeto, seguido del
nombre del campo, como en objectname._fieldName . Por ejemplo:
CalendarEntry birthday = new CalendarEntry();
birthday.Day = "Saturday";
Se puede proporcionar un valor inicial a un campo mediante el operador de asignación cuando se declara el
campo. Para asignar automáticamente el campo Day a "Monday" , por ejemplo, se declararía Day como en el
ejemplo siguiente:
Los campos se inicializan inmediatamente antes de que se llame al constructor para la instancia del objeto. Si el
constructor asigna el valor de un campo, sobrescribirá cualquier valor dado durante la declaración del campo.
Para obtener más información, vea Utilizar constructores.
NOTE
Un inicializador de campo no puede hacer referencia a otros campos de instancia.
Se pueden marcar campos como público, privado, protegido, interno, protegido interno o privado protegido.
Estos modificadores de acceso definen cómo los usuarios de la clase pueden obtener acceso a los campos. Para
obtener más información, consulte Modificadores de acceso.
Opcionalmente, un campo se puede declarar como static. Esto hace que el campo esté disponible para los
llamadores en cualquier momento, aunque no exista ninguna instancia de la clase. Para más información, vea
Clases estáticas y sus miembros.
Se puede declarar un campo como readonly. Solamente se puede asignar un valor a un campo de solo lectura
durante la inicialización o en un constructor. Un campo static readonly es muy similar a una constante, salvo
que el compilador de C# no tiene acceso al valor de un campo estático de solo lectura en tiempo de
compilación, solo en tiempo de ejecución. Para obtener más información, vea Constants (Constantes).
Consulte también
Guía de programación de C#
Clases, estructuras y registros
Utilizar constructores
Herencia
Modificadores de acceso
Clases y miembros de clase abstractos y sellados
Constantes (Guía de programación de C#)
16/09/2021 • 2 minutes to read
Las constantes son valores inmutables que se conocen en tiempo de compilación y que no cambian durante la
vida del programa. Las constantes se declaran con el modificador const. Solo los tipos integrados de C#
(excluido System.Object) pueden declararse como const . Los tipos definidos por el usuario, incluidas las clases,
las estructuras y las matrices, no pueden ser const . Use el modificador readonly para crear una clase, una
estructura o una matriz que se inicialice una vez en tiempo de ejecución (por ejemplo, en un constructor) y que
posteriormente no se pueda cambiar.
C# no admite métodos, propiedades ni eventos const .
El tipo enum permite definir constantes con nombre para los tipos integrados enteros (por ejemplo, int , uint ,
long , etc.). Para más información, vea enum.
class Calendar1
{
public const int Months = 12;
}
En este ejemplo la constante Months siempre es 12 y ni siquiera la propia clase la puede cambiar. De hecho,
cuando el compilador detecta un identificador de constante en el código fuente de C# (por ejemplo, Months ),
sustituye directamente el valor literal en el código de lenguaje intermedio (IL) que genera. Dado que no hay
ninguna dirección de variable asociada a una constante en tiempo de ejecución, no se pueden pasar los campos
const por referencia ni pueden aparecer como un valor L en una expresión.
NOTE
Tenga cuidado al hacer referencia a valores de constante definidos en otro código como archivos DLL. Si una nueva
versión del archivo DLL define un nuevo valor para la constante, el programa conservará el valor literal anterior hasta que
se vuelva a compilar con la versión nueva.
Se pueden declarar varias constantes del mismo tipo a la vez, por ejemplo:
class Calendar2
{
public const int Months = 12, Weeks = 52, Days = 365;
}
La expresión que se usa para inicializar una constante puede hacer referencia a otra constante si no crea una
referencia circular. Por ejemplo:
class Calendar3
{
public const int Months = 12;
public const int Weeks = 52;
public const int Days = 365;
Las constantes pueden marcarse como públicas, privadas, protegidas, internas, protegidas internaso privadas
protegidas. Estos modificadores de acceso definen cómo los usuarios de la clase pueden acceder a la constante.
Para más información, vea Modificadores de acceso.
A las constantes se accede como si fueran campos estáticos porque el valor de la constante es el mismo para
todas las instancias del tipo. No use la palabra clave static para declararlas. Las expresiones que no están en la
clase que define la constante deben usar el nombre de la clase, un punto y el nombre de la constante para
acceder a ella. Por ejemplo:
Vea también
Guía de programación de C#
Clases, estructuras y registros
Propiedades
Tipos
readonly
Inmutabilidad en la primera parte de C#: tipos de inmutabilidad
Procedimiento para definir propiedades abstractas
(Guía de programación de C#)
16/09/2021 • 2 minutes to read
En el ejemplo siguiente se muestra cómo definir las propiedades abstract. Una declaración de propiedad
abstracta no proporciona una implementación de los descriptores de acceso de propiedad, declara que la clase
admite propiedades, pero deja la implementación del descriptor de acceso a las clases derivadas. En el ejemplo
siguiente se muestra cómo implementar las propiedades abstractas heredadas de una clase base.
Este ejemplo consta de tres archivos, cada uno de los cuales se compila individualmente y se hace referencia a
su ensamblado resultante mediante la siguiente compilación:
abstractshape.cs: la clase Shape que contiene una propiedad Area abstracta.
shapes.cs: las subclases de la clase Shape .
shapetest.cs: un programa de prueba para mostrar las áreas de algunos objetos derivados de Shape .
Para compilar el ejemplo, use el siguiente comando:
csc abstractshape.cs shapes.cs shapetest.cs
Ejemplos
Este archivo declara la clase Shape que contiene la propiedad Area del tipo double .
// compile with: csc -target:library abstractshape.cs
public abstract class Shape
{
private string name;
public Shape(string s)
{
// calling the set accessor of the Id property.
Id = s;
}
public string Id
{
get
{
return name;
}
set
{
name = value;
}
}
Al declarar una propiedad abstracta (como Area en este ejemplo), simplemente indica qué descriptores
de acceso de propiedad están disponibles, pero no los implementa. En este ejemplo, solo está disponible
un descriptor de acceso get, por lo que la propiedad es de solo lectura.
En el siguiente código se muestran tres subclases de Shape y cómo invalidan la propiedad Area para
proporcionar su propia implementación.
// compile with: csc -target:library -reference:abstractshape.dll shapes.cs
public class Square : Shape
{
private int side;
En el siguiente código se muestra un programa de prueba que crea un número de objetos derivados de Shape
e imprime sus áreas.
// compile with: csc -reference:abstractshape.dll;shapes.dll shapetest.cs
class TestClass
{
static void Main()
{
Shape[] shapes =
{
new Square(5, "Square #1"),
new Circle(3, "Circle #1"),
new Rectangle( 4, 5, "Rectangle #1")
};
System.Console.WriteLine("Shapes Collection");
foreach (Shape s in shapes)
{
System.Console.WriteLine(s);
}
}
}
/* Output:
Shapes Collection
Square #1 Area = 25.00
Circle #1 Area = 28.27
Rectangle #1 Area = 20.00
*/
Vea también
Guía de programación de C#
Clases, estructuras y registros
Clases y miembros de clase abstractos y sellados
Propiedades
Definición de constantes en C#
16/09/2021 • 2 minutes to read
Las constantes son campos cuyos valores se establecen en tiempo de compilación y nunca se pueden cambiar.
Use constantes para proporcionar nombres descriptivos en lugar de literales numéricos ("números mágicos")
para valores especiales.
NOTE
En C#, la directiva del preprocesador #define no puede usarse para definir constantes en la manera que se usa
normalmente en C y C++.
Para definir valores constantes de tipos enteros ( int , byte y así sucesivamente) use un tipo enumerado. Para
más información, vea enum.
Para definir constantes no enteras, un enfoque es agruparlas en una única clase estática denominada Constants
. Esto necesitará que todas las referencias a las constantes vayan precedidas por el nombre de clase, como se
muestra en el ejemplo siguiente.
Ejemplo
using System;
class Program
{
static void Main()
{
double radius = 5.3;
double area = Constants.Pi * (radius * radius);
int secsFromSun = 149476000 / Constants.SpeedOfLight; // in km
Console.WriteLine(secsFromSun);
}
}
El uso del calificador de nombre de clase ayuda a garantizar que usted y otros usuarios que usan la constante
comprenden que es una constante y que no puede modificarse.
Consulte también
Clases, estructuras y registros
Propiedades (Guía de programación de C#)
16/09/2021 • 5 minutes to read
Una propiedad es un miembro que proporciona un mecanismo flexible para leer, escribir o calcular el valor de
un campo privado. Las propiedades se pueden usar como si fueran miembros de datos públicos, pero en
realidad son métodos especiales denominados descriptores de acceso. Esto permite acceder fácilmente a los
datos a la vez que proporciona la seguridad y la flexibilidad de los métodos.
class TimePeriod
{
private double _seconds;
class Program
{
static void Main()
{
TimePeriod t = new TimePeriod();
// The property assignment causes the 'set' accessor to be called.
t.Hours = 24;
A partir de C# 7.0, los descriptores de acceso get y set se pueden implementar como miembros con forma
de expresión. En este caso, las palabras clave get y set deben estar presentes. En el ejemplo siguiente se
muestra el uso de definiciones de cuerpos de expresión para ambos descriptores de acceso. Observe que no se
usa la palabra clave return con el descriptor de acceso get .
using System;
class Program
{
static void Main(string[] args)
{
var item = new SaleItem("Shoes", 19.95m);
Console.WriteLine($"{item.Name}: sells for {item.Price:C2}");
}
}
// The example displays output like the following:
// Shoes: sells for $19.95
class Program
{
static void Main(string[] args)
{
var item = new SaleItem{ Name = "Shoes", Price = 19.95m };
Console.WriteLine($"{item.Name}: sells for {item.Price:C2}");
}
}
// The example displays output like the following:
// Shoes: sells for $19.95
Secciones relacionadas
Utilizar propiedades
Propiedades de interfaz
Comparación entre propiedades e indizadores
Restringir la accesibilidad del descriptor de acceso
Propiedades autoimplementadas
Vea también
Guía de programación de C#
Utilizar propiedades
Indizadores
get (Palabra clave)
set (palabra clave)
Utilizar propiedades (Guía de programación de C#)
16/09/2021 • 9 minutes to read
Las propiedades combinan aspectos de los campos y los métodos. Para el usuario de un objeto, una propiedad
que parece un campo, el acceso a la propiedad necesita la misma sintaxis. Para el implementador de una clase,
una propiedad es uno o dos bloques de código que representa un descriptor de acceso get o un descriptor de
acceso set. El bloque de código del descriptor de acceso get se ejecuta cuando se lee la propiedad; el bloque de
código del descriptor de acceso set se ejecuta cuando se asigna un nuevo valor a la propiedad. Una propiedad
sin un descriptor de acceso set se considera de solo lectura. Una propiedad sin un descriptor de acceso get
se considera de solo escritura. Una propiedad que tiene ambos descriptores de acceso es de lectura y escritura.
En C# 9 y versiones posteriores, puede usar un descriptor de acceso init en lugar de set para que la
propiedad sea de solo lectura.
A diferencia de los campos, las propiedades no se clasifican como variables. Por lo tanto, no puede pasar una
propiedad como un parámetro ref u out.
Las propiedades tienen muchos usos: pueden validar datos antes de permitir un cambio; pueden exponer
claramente datos en una clase donde esos datos se recuperan realmente de otros orígenes, como una base de
datos; pueden realizar una acción cuando los datos se cambian, como generar un evento, o cambiar el valor de
otros campos.
Las propiedades se declaran en el bloque de clase especificando el nivel de acceso del campo, seguido del tipo
de la propiedad, seguido del nombre de la propiedad y seguido de un bloque de código que declara un
descriptor de acceso get o un descriptor de acceso set . Por ejemplo:
En este ejemplo, Month se declara como una propiedad, de manera que el descriptor de acceso set pueda
estar seguro de que el valor Month se establece entre 1 y 12. La propiedad Month usa un campo privado para
realizar un seguimiento del valor actual. A menudo, a la ubicación real de los datos de una propiedad se le
conoce como la "memoria auxiliar" de la propiedad. Esto es común para las propiedades que usan campos
privados como una memoria auxiliar. El campo se marca como privado para asegurarse de que solo puede
cambiarse llamando a la propiedad. Para obtener más información sobre las restricciones de acceso público y
privado, vea Modificadores de acceso.
Las propiedades implementadas automáticamente proporcionan una sintaxis simplificada para las declaraciones
de propiedad simples. Para obtener más información, vea Propiedades implementadas automáticamente.
El descriptor de acceso get
El cuerpo del descriptor de acceso get se parece al de un método. Debe devolver un valor del tipo de
propiedad. La ejecución del descriptor de acceso get es equivalente a la lectura del valor del campo. Por
ejemplo, cuando se devuelve la variable privada del descriptor de acceso get y se habilitan las optimizaciones,
la llamada al método de descriptor de acceso get se inserta mediante el compilador, de manera que no existe
ninguna sobrecarga de llamada al método. En cambio, un método de descriptor de acceso get virtual no puede
insertarse porque el compilador no conoce en tiempo de compilación a qué método puede llamarse realmente
en tiempo de ejecución. A continuación se muestra un descriptor de acceso get que devuelve el valor de un
campo privado _name :
class Person
{
private string _name; // the name field
public string Name => _name; // the Name property
}
Cuando hace referencia a la propiedad, excepto como el destino de una asignación, el descriptor de acceso get
se invoca para leer el valor de la propiedad. Por ejemplo:
El descriptor de acceso get debe finalizar en una instrucción return o throw, y el control no puede salir del
cuerpo del descriptor de acceso.
Cambiar el estado del objeto mediante el descriptor de acceso get es un estilo de programación incorrecto. Por
ejemplo, el siguiente descriptor de acceso produce el efecto secundario de cambiar el estado del objeto cada vez
que se tiene acceso al campo _number .
El descriptor de acceso get puede usarse para devolver el valor de campo o para calcularlo y devolverlo. Por
ejemplo:
class Employee
{
private string _name;
public string Name => _name != null ? _name : "NA";
}
Cuando asigna un valor a la propiedad, el descriptor de acceso set se invoca mediante un argumento que
proporciona el valor nuevo. Por ejemplo:
Es un error usar el nombre de parámetro implícito, value , para una declaración de variable local en el
descriptor de acceso set .
Observaciones
Las propiedades se pueden marcar como public , private , protected , internal , protected internal o
private protected . Estos modificadores de acceso definen cómo los usuarios de la clase pueden obtener acceso
a la propiedad. Los descriptores de acceso get y set para la misma propiedad pueden tener diferentes
modificadores de acceso. Por ejemplo, get puede ser public para permitir el acceso de solo lectura desde el
exterior del tipo, y set puede ser private o protected . Para obtener más información, consulte Modificadores
de acceso.
Una propiedad puede declararse como una propiedad estática mediante la palabra clave static . Esto hace que
la propiedad esté disponible para los autores de la llamada en cualquier momento, aunque no exista ninguna
instancia de la clase. Para más información, vea Clases estáticas y sus miembros.
Una propiedad puede marcarse como una propiedad virtual mediante la palabra clave virtual. Esto permite que
las clases derivadas invaliden el comportamiento de la propiedad mediante la palabra clave override. Para
obtener más información sobre estas opciones, vea Herencia.
Una propiedad que invalida una propiedad virtual también puede sellarse, que especifica que para las clases
derivadas ya no es virtual. Por último, una propiedad puede declararse abstracta. Esto significa que no existe
ninguna implementación en la clase, y las clases derivadas deben escribir su propia implementación. Para
obtener más información sobre estas opciones, vea Clases y miembros de clase abstractos y sellados (Guía de
programación de C#).
NOTE
Es un error usar un modificador virtual, abstract u override en un descriptor de acceso de una propiedad static.
Ejemplos
En este ejemplo se muestran las propiedades de solo lectura, estáticas y de instancia. Acepta el nombre del
empleado desde el teclado, incrementa NumberOfEmployees en 1 y muestra el nombre del empleado y el número.
// A Constructor:
public Employee() => _counter = ++NumberOfEmployees; // Calculate the employee's number:
}
class TestEmployee
{
static void Main()
{
Employee.NumberOfEmployees = 107;
Employee e1 = new Employee();
e1.Name = "Claude Vige";
class TestHiding
{
static void Main()
{
Manager m1 = new Manager();
La conversión (Employee) se usa para tener acceso a la propiedad oculta de la clase base:
((Employee)m1).Name = "Mary";
Para obtener más información sobre cómo ocultar miembros, vea el Modificador new.
//constructor
public Square(double s) => side = s;
//constructor
public Cube(double s) => side = s;
class TestShapes
{
static void Main()
{
// Input the side:
System.Console.Write("Enter the side: ");
double side = double.Parse(System.Console.ReadLine());
Vea también
Guía de programación de C#
Propiedades
Propiedades de interfaz
Propiedades autoimplementadas
Propiedades de interfaces (Guía de programación
de C#)
16/09/2021 • 2 minutes to read
Las propiedades se pueden declarar en una interfaz. En el ejemplo siguiente se declara un descriptor de acceso
de propiedad de interfaz:
Normalmente, las propiedades de interfaz no tienen un cuerpo. Los descriptores de acceso indican si la
propiedad es de lectura y escritura, de solo lectura o de solo escritura. A diferencia de las clases y las estructuras,
la declaración de los descriptores de acceso sin cuerpo no declara una propiedad implementada
automáticamente. A partir de C# 8.0, una interfaz puede definir una implementación predeterminada para los
miembros, incluidas las propiedades. La definición de una implementación predeterminada para una propiedad
en una interfaz es poco frecuente, ya que las interfaces no pueden definir campos de datos de instancia.
Ejemplo
En este ejemplo, la interfaz IEmployee tiene una propiedad de lectura y escritura, Name , y una propiedad de solo
lectura, Counter . La clase Employee implementa la interfaz IEmployee y usa estas dos propiedades. El
programa lee el nombre de un nuevo empleado y el número actual de empleados y muestra el nombre del
empleado y el número de empleados calculado.
Puede usar el nombre completo de la propiedad, que hace referencia a la interfaz en la que se declara el
miembro. Por ejemplo:
string IEmployee.Name
{
get { return "Employee Name"; }
set { }
}
En el ejemplo anterior se muestra la implementación de interfaz explícita. Por ejemplo, si la clase Employee
implementa dos interfaces ICitizen y IEmployee , y ambas interfaces tienen la propiedad Name , la
implementación del miembro de interfaz explícita será necesaria. Es decir, la siguiente declaración de propiedad:
string IEmployee.Name
{
get { return "Employee Name"; }
set { }
}
interface IEmployee
{
string Name
{
get;
set;
}
int Counter
{
get;
}
}
// constructor
public Employee() => _counter = ++numberOfEmployees;
}
Salida de ejemplo
Enter number of employees: 210
Enter the name of the new employee: Hazem Abolrous
The employee information:
Employee number: 211
Employee name: Hazem Abolrous
Consulte también
Guía de programación de C#
Propiedades
Utilizar propiedades
Comparación entre propiedades e indizadores
Indizadores
Interfaces
Restringir la accesibilidad del descriptor de acceso
(Guía de programación de C#)
16/09/2021 • 4 minutes to read
Las partes get y set de una propiedad o un indexador se denominan descriptores de acceso. De forma
predeterminada, estos descriptores de acceso tienen la misma visibilidad o nivel de acceso de la propiedad o el
indexador al que pertenecen. Para obtener más información, vea Niveles de accesibilidad. En cambio, a veces
resulta útil restringir el acceso a uno de estos descriptores de acceso. Normalmente, esto implica restringir la
accesibilidad del descriptor de acceso set , mientras que se mantiene el descriptor de acceso get accesible
públicamente. Por ejemplo:
En este ejemplo, una propiedad denominada Name define un descriptor de acceso get y set . El descriptor de
acceso get recibe el nivel de accesibilidad de la propiedad, public en este caso, mientras que el descriptor de
acceso set se restringe explícitamente al aplicar el modificador de acceso protected al propio descriptor de
acceso.
Implementar interfaces
Al usar un descriptor de acceso para implementar una interfaz, este no puede tener un modificador de acceso.
En cambio, si implementa la interfaz con un descriptor de acceso, como get , el otro descriptor de acceso puede
tener un modificador de acceso, como en el ejemplo siguiente:
Ejemplo
En el ejemplo siguiente, se incluyen tres clases: BaseClass , DerivedClass y MainClass . Hay dos propiedades en
BaseClass , Name y Id en ambas clases. En el ejemplo, se muestra cómo la propiedad Id en DerivedClass se
puede ocultar con la propiedad Id en BaseClass al usar un modificador de acceso restrictivo como protected o
private. Por tanto, cuando asigna valores a esta propiedad, se llama en su lugar a la propiedad en la clase
BaseClass . Si se reemplaza el modificador de acceso por public, la propiedad será accesible.
En el ejemplo, también se muestra que un modificador de acceso restrictivo, como private o protected , en el
descriptor de acceso set de la propiedad Name en DerivedClass impide el acceso al descriptor de acceso y
genera un error al asignárselo.
public string Id
{
get { return _id; }
set { }
}
}
class MainClass
{
static void Main()
{
BaseClass b1 = new BaseClass();
DerivedClass d1 = new DerivedClass();
b1.Name = "Mary";
d1.Name = "John";
b1.Id = "Mary123";
d1.Id = "John123"; // The BaseClass.Id property is called.
Comentarios
Tenga en cuenta que, si reemplaza la declaración new private string Id por new public string Id , obtendrá el
resultado:
Name and ID in the base class: Name-BaseClass, ID-BaseClass
Vea también
Guía de programación de C#
Propiedades
Indizadores
Modificadores de acceso
Procedimiento para declarar y usar propiedades de
lectura y escritura (Guía de programación de C#)
16/09/2021 • 3 minutes to read
Las propiedades proporcionan la comodidad de los miembros de datos públicos sin los riesgos que provienen
del acceso sin comprobar, sin controlar y sin proteger a los datos de un objeto. Esto se consigue mediante los
descriptores de acceso: métodos especiales que asignan y recuperan valores del miembro de datos subyacente.
El descriptor de acceso set permite que los miembros de datos se asignen, y el descriptor de acceso get recupera
los valores de los miembros de datos.
En este ejemplo se muestra una clase Person que tiene dos propiedades: Name (string) y Age (int). Ambas
propiedades proporcionan descriptores de acceso get y set , de manera que se consideran propiedades de
lectura y escritura.
Ejemplo
class Person
{
private string _name = "N/A";
private int _age = 0;
set
{
_age = value;
}
}
class TestPerson
{
static void Main()
{
{
// Create a new Person object:
Person person = new Person();
// Print out the name and the age associated with the person:
Console.WriteLine("Person details - {0}", person);
Programación sólida
En el ejemplo anterior, las propiedades Name y Age son públicas e incluyen un descriptor de acceso get y
set . Esto permite que cualquier objeto lea y escriba estas propiedades. En cambio, a veces esto es conveniente
para excluir uno de los descriptores de acceso. Omitir el descriptor de acceso set , por ejemplo, hace que la
propiedad sea de solo lectura:
De manera alternativa, puede exponer un descriptor de acceso públicamente pero hacer que el otro sea privado
o esté protegido. Para obtener más información, vea Accesibilidad del descriptor de acceso asimétrico.
Una vez que se declaren las propiedades, pueden usarse como si fueran campos de la clase. Esto permite una
sintaxis muy natural cuando ambos obtienen y establecen el valor de una propiedad, como se muestra en las
instrucciones siguientes:
person.Name = "Joe";
person.Age = 99;
Tenga en cuenta que en un método set de la propiedad está disponible una variable value especial. Esta
variable contiene el valor que el usuario ha especificado, por ejemplo:
_name = value;
Tenga en cuenta la sintaxis pura para incrementar la propiedad Age en un objeto Person :
person.Age += 1;
Si los métodos set y get independientes se han usado para modelar las propiedades, el código equivalente
puede tener este aspecto:
person.SetAge(person.GetAge() + 1);
Tenga en cuenta que ToString no se usa explícitamente en el programa. Se invoca de manera predeterminada
mediante las llamadas a WriteLine .
Vea también
Guía de programación de C#
Propiedades
Clases, estructuras y registros
Propiedades autoimplementadas (Guía de
programación de C#)
16/09/2021 • 2 minutes to read
En C# 3.0 y versiones posteriores, las propiedades implementadas automáticamente hacen que la declaración
de propiedades sea más concisa cuando no es necesaria ninguna lógica adicional en los descriptores de acceso
de la propiedad. También permite que el código de cliente cree objetos. Cuando se declara una propiedad tal
como se muestra en el ejemplo siguiente, el compilador crea un campo de respaldo privado y anónimo al que
solo se puede acceder con los descriptores de acceso de propiedad get y set . En C# 9 y versiones posteriores,
los descriptores de acceso init también se pueden declarar como propiedades implementadas
automáticamente.
Ejemplo
En el ejemplo siguiente se muestra una clase simple que tiene algunas propiedades implementadas
automáticamente:
// Constructor
public Customer(double purchases, string name, int id)
{
TotalPurchases = purchases;
Name = name;
CustomerId = id;
}
// Methods
public string GetContactInfo() { return "ContactInfo"; }
public string GetTransactionHistory() { return "History"; }
class Program
{
static void Main()
{
// Intialize a new object.
Customer cust1 = new Customer(4987.63, "Northwind", 90108);
// Modify a property.
cust1.TotalPurchases += 499.99;
}
}
La clase que se muestra en el ejemplo anterior es mutable. El código de cliente puede cambiar los valores de los
objetos una vez creados. En clases complejas que contienen el comportamiento importante (métodos) y los
datos, suele ser necesario tener propiedades públicas. Pero para las clases pequeñas o estructuras que solo
encapsulan un conjunto de valores (datos) y tienen poco o ningún comportamiento, debe usar una de las
opciones siguientes para hacer que los objetos sean inmutables:
Declare solo un descriptor de acceso get (inmutable en cualquier lugar excepto el constructor).
Declare un descriptor de acceso get y un descriptor de acceso init (inmutable en cualquier lugar excepto
durante la construcción del objeto).
Declare el descriptor de acceso set como private (inmutable para los consumidores).
Para obtener más información, vea Procedimiento para implementar una clase ligera con propiedades
autoimplementadas.
Consulte también
Propiedades
Modificadores
Procedimiento para implementar una clase ligera
con propiedades autoimplementadas (Guía de
programación de C#)
16/09/2021 • 3 minutes to read
En este ejemplo se muestra cómo crear una clase ligera inmutable que solo sirve para encapsular un conjunto
de propiedades autoimplementadas. Use este tipo de construcción en lugar de un struct cuando deba utilizar
una semántica de tipo de referencia.
Puede hacer que una propiedad sea inmutable de estas maneras:
Declare solo el descriptor de acceso get, que hace que la propiedad sea inmutable en cualquier lugar
excepto en el constructor del tipo.
Declare un descriptor de acceso init en lugar de set , que hace que la propiedad se pueda establecer solo
en el constructor o mediante un inicializador de objeto.
Declare el descriptor de acceso set como private. La propiedad solo se puede establecer dentro del tipo,
pero es inmutable para los consumidores.
Cuando se declara un descriptor de acceso set privado, no se puede usar un inicializador de objeto para
inicializar la propiedad. Se debe utilizar un constructor o un método factory.
En el ejemplo siguiente se muestra cómo una propiedad con solo el descriptor de acceso get es distinta de una
con get y un conjunto privado.
class Contact
{
public string Name { get; }
public string Address { get; private set; }
Ejemplo
En el siguiente ejemplo se muestran dos maneras de implementar una clase inmutable que tenga propiedades
autoimplementadas. Cada forma declara una de las propiedades con un set privado y una de las propiedades
solamente con un get . La primera clase usa un constructor solo para inicializar las propiedades y la segunda
clase utiliza un método factory estático que llama a un constructor.
// Public constructor.
public Contact(string contactName, string contactAddress)
{
Name = contactName;
Address = contactAddress;
}
}
// Read-only property.
public string Address { get; }
// Private constructor.
private Contact2(string contactName, string contactAddress)
{
Name = contactName;
Address = contactAddress;
}
/* Output:
Terry Adams, 123 Main St.
Fadi Fakhouri, 345 Cypress Ave.
Hanying Feng, 678 1st Ave
Cesar Garcia, 12 108th St.
Debra Garcia, 89 E. 42nd St.
*/
El compilador crea campos de respaldo para cada propiedad autoimplementada. No se puede acceder a los
campos directamente desde el código fuente.
Consulte también
Propiedades
struct
Inicializadores de objeto y colección
Métodos (Guía de programación de C#)
16/09/2021 • 11 minutes to read
Un método es un bloque de código que contiene una serie de instrucciones. Un programa hace que se ejecuten
las instrucciones al llamar al método y especificando los argumentos de método necesarios. En C#, todas las
instrucciones ejecutadas se realizan en el contexto de un método.
El método Main es el punto de entrada para cada aplicación de C# y se llama mediante Common Language
Runtime (CLR) cuando se inicia el programa. En una aplicación que usa instrucciones de nivel superior, el
compilador genera el método Main y contiene todas las instrucciones de nivel superior.
NOTE
En este artículo se analizan los métodos denominados. Para obtener información sobre las funciones anónimas, vea
Funciones anónimas.
Firmas de método
Los métodos se declaran en una clase, struct o interfaz especificando el nivel de acceso, como public o
private , modificadores opcionales como abstract o sealed , el valor devuelto, el nombre del método y
cualquier parámetro de método. Todas estas partes forman la firma del método.
IMPORTANT
Un tipo de valor devuelto de un método no forma parte de la firma del método con el objetivo de sobrecargar el método.
Sin embargo, forma parte de la firma del método al determinar la compatibilidad entre un delegado y el método que
señala.
Los parámetros de método se encierran entre paréntesis y se separan por comas. Los paréntesis vacíos indican
que el método no requiere parámetros. Esta clase contiene cuatro métodos:
Acceso a métodos
Llamar a un método en un objeto es como acceder a un campo. Después del nombre del objeto, agregue un
punto, el nombre del método y paréntesis. Los argumentos se enumeran entre paréntesis y están separados por
comas. Los métodos de la clase Motorcycle se pueden llamar como en el ejemplo siguiente:
class TestMotorcycle : Motorcycle
{
moto.StartEngine();
moto.AddGas(15);
moto.Drive(5, 20);
double speed = moto.GetTopSpeed();
Console.WriteLine("My top speed is {0}", speed);
}
}
int Square(int i)
{
// Store input argument in a local variable.
int input = i;
return input * input;
}
Ahora, si se pasa un objeto basado en este tipo a un método, también se pasa una referencia al objeto. En el
ejemplo siguiente se pasa un objeto de tipo SampleRefType al método ModifyObject :
Fundamentalmente, el ejemplo hace lo mismo que el ejemplo anterior en el que se pasa un argumento por
valor a un método. Pero, debido a que se utiliza un tipo de referencia, el resultado es diferente. La modificación
que se lleva a cabo en ModifyObject al campo value del parámetro, obj , también cambia el campo value del
argumento, rt , en el método TestRefType . El método TestRefType muestra 33 como salida.
Para obtener más información sobre cómo pasar tipos de referencia por valor y por referencia, vea Pasar
parámetros Reference-Type (Guía de programación de C#) y Tipos de referencia (Referencia de C#).
Valores devueltos
Los métodos pueden devolver un valor al autor de llamada. Si el tipo de valor devuelto (el tipo que aparece
antes del nombre de método) no es void , el método puede devolver el valor mediante la palabra clave return .
Una instrucción con la palabra clave return seguida de un valor que coincide con el tipo de valor devuelto
devolverá este valor al autor de llamada del método.
El valor puede devolverse al autor de la llamada mediante valor o, a partir de C# 7.0, mediante referencia. Los
valores se devuelven al autor de la llamada mediante referencia si la palabra clave ref se usa en la firma del
método y sigue cada palabra clave return . Por ejemplo, la siguiente firma del método y la instrucción return
indican que el método devuelve una variable denominada estDistance mediante referencia al autor de la
llamada.
La palabra clave return también detiene la ejecución del método. Si el tipo de valor devuelto es void , una
instrucción return sin un valor también es útil para detener la ejecución del método. Sin la palabra clave
return , el método dejará de ejecutarse cuando alcance el final del bloque de código. Los métodos con un tipo
de valor devuelto no nulo son necesarios para usar la palabra clave return para devolver un valor. Por ejemplo,
estos dos métodos utilizan la palabra clave return para devolver enteros:
class SimpleMath
{
public int AddTwoNumbers(int number1, int number2)
{
return number1 + number2;
}
Para utilizar un valor devuelto de un método, el método de llamada puede usar la llamada de método en
cualquier lugar; un valor del mismo tipo sería suficiente. También puede asignar el valor devuelto a una variable.
Por ejemplo, los dos siguientes ejemplos de código logran el mismo objetivo:
Usar una variable local, en este caso, result , para almacenar un valor es opcional. La legibilidad del código
puede ser útil, o puede ser necesaria si debe almacenar el valor original del argumento para todo el ámbito del
método.
Para usar un valor devuelto mediante referencia de un método, debe declarar una variable local de tipo ref si
pretende modificar su valor. Por ejemplo, si el método Planet.GetEstimatedDistance devuelve un valor Double
mediante referencia, puede definirlo como una variable local de tipo ref con código como el siguiente:
Devolver una matriz multidimensional de un método, M , que modifica el contenido de la matriz no es necesario
si la función de llamada ha pasado la matriz a M . Puede devolver la matriz resultante de M para obtener un
estilo correcto o un flujo funcional de valores, pero no es necesario porque C# pasa todos los tipos de referencia
mediante valor, y el valor de una referencia de matriz es el puntero de la matriz. En el método M , los cambios en
el contenido de la matriz los puede observar cualquier código que tenga una referencia a la matriz, como se
muestra en el ejemplo siguiente:
static void Main(string[] args)
{
int[,] matrix = new int[2, 2];
FillMatrix(matrix);
// matrix is now full of -1
}
Métodos asincrónicos
Mediante la característica asincrónica, puede invocar métodos asincrónicos sin usar definiciones de llamada
explícitas ni dividir manualmente el código en varios métodos o expresiones lambda.
Si marca un método con el modificador async, puede usar el operador await en el método. Cuando el control
alcanza una expresión await en el método asincrónico, el control se devuelve al autor de llamada y se progreso
del método se suspende hasta que se completa la tarea esperada. Cuando se completa la tarea, la ejecución
puede reanudarse en el método.
NOTE
Un método asincrónico vuelve al autor de la llamada cuando encuentra el primer objeto esperado que aún no se ha
completado o cuando llega al final del método asincrónico, lo que ocurra primero.
class Program
{
static Task Main() => DoSomethingAsync();
Console.WriteLine($"Result: {result}");
}
Un método aisncrónico no puede declarar ningún parámetro ref u out , pero puede llamar a los métodos que
tienen estos parámetros.
Para obtener más información sobre los métodos asincrónicos, consulte los artículos Programación asincrónica
con async y await y Tipos de valor devueltos asincrónicos.
public Point Move(int dx, int dy) => new Point(x + dx, y + dy);
public void Print() => Console.WriteLine(First + " " + Last);
// Works with operators, properties, and indexers too.
public static Complex operator +(Complex a, Complex b) => a.Add(b);
public string Name => First + " " + Last;
public Customer this[long id] => store.LookupCustomer(id);
Si el método devuelve void o si es un método asincrónico, el cuerpo del método debe ser una expresión de
instrucción (igual que con las expresiones lambda). Para las propiedades y los indexadores, solo deben leerse, y
no usa la palabra clave de descriptor de acceso get .
Iterators
Un iterador realiza una iteración personalizada en una colección, como una lista o matriz. Un iterador utiliza la
instrucción yield return para devolver cada elemento de uno en uno. Cuando se alcanza la instrucción yield
return , se recuerda la ubicación actual en el código. La ejecución se reinicia desde esa ubicación la próxima vez
que se llama el iterador.
Llame a un iterador a partir del código de cliente mediante una instrucción foreach .
El tipo de valor devuelto de un iterador puede ser IEnumerable, IEnumerable<T>, IEnumerator o
IEnumerator<T>.
Para obtener más información, consulta Iteradores.
Vea también
Guía de programación de C#
Clases, estructuras y registros
Modificadores de acceso
Clases estáticas y sus miembros
Herencia
Clases y miembros de clase abstractos y sellados
params
return
out
ref
Pasar parámetros
Funciones locales (Guía de programación de C#)
16/09/2021 • 10 minutes to read
A partir de C# 7.0, C# admite funciones locales. Las funciones locales son métodos privados de un tipo que
están anidados en otro miembro. Solo se pueden llamar desde su miembro contenedor. Las funciones locales se
pueden declarar en y llamar desde:
Métodos, especialmente los métodos de iterador y asincrónicos
Constructores
Descriptores de acceso de propiedad
Descriptores de acceso de un evento
Métodos anónimos
Expresiones lambda
Finalizadores
Otras funciones locales
En cambio, las funciones locales no se pueden declarar dentro de un miembro con forma de expresión.
NOTE
En algunos casos, puede usar una expresión lambda para implementar funcionalidad compatible también con una función
local. Para ver una comparación, consulte Funciones locales frente a expresiones lambda.
Las funciones locales aclaran la intención del código. Cualquiera que lea el código puede ver que solo el método
que lo contiene puede llamar al método. Para los proyectos de equipo, también hacen que sea imposible que
otro desarrollador llame erróneamente al método de forma directa desde cualquier otro punto de la clase o
estructura.
Todas las variables locales que se definen en el miembro contenedor, incluidos sus parámetros de método, son
accesibles en la función local no estática.
A diferencia de una definición de método, una definición de función local no puede incluir el modificador de
acceso de los miembros. Dado que todas las funciones locales son privadas, incluido un modificador de acceso,
como la palabra clave private , se genera el error del compilador CS0106, "El modificador 'private' no es válido
para este elemento".
En el ejemplo siguiente, se define una función local denominada AppendPathSeparator que es privada a un
método denominado GetText :
A partir de C# 9.0, puede aplicar atributos a una función local, sus parámetros y parámetros de tipo, como se
muestra en el ejemplo siguiente:
#nullable enable
private static void Process(string?[] lines, string mark)
{
foreach (var line in lines)
{
if (IsValid(line))
{
// Processing logic...
}
}
En el ejemplo anterior se usa un atributo especial para ayudar al compilador en el análisis estático en un
contexto que acepta valores NULL.
Si coloca la lógica de iterador en una función local, se iniciarán excepciones de validación de argumentos al
recuperar el enumerador, como se muestra en el ejemplo siguiente:
using System;
using System.Collections.Generic;
return GetOddSequenceEnumerator();
IEnumerable<int> GetOddSequenceEnumerator()
{
for (int i = start; i <= end; i++)
{
if (i % 2 == 1)
yield return i;
}
}
}
}
// The example displays the output like this:
//
// Unhandled exception. System.ArgumentOutOfRangeException: end must be less than or equal to 100.
(Parameter 'end')
// at IteratorWithLocalExample.OddSequence(Int32 start, Int32 end) in IteratorWithLocal.cs:line 22
// at IteratorWithLocalExample.Main() in IteratorWithLocal.cs:line 8
return nthFactorial(n);
}
Nomenclatura
Las funciones locales se nombran explícitamente como métodos. Las expresiones lambda son métodos
anónimos y deben asignarse a variables de un tipo delegate , normalmente los tipos Action o Func . Cuando
se declara una función local, el proceso es como escribir un método normal: se declaran un tipo de valor
devuelto y una signatura de función.
Signaturas de función y tipos de expresiones lambda
Las expresiones lambda se basan en el tipo de la variable Action / Func al que están asignadas para determinar
los tipos de argumento y de valor devuelto. En las funciones locales, dado que la sintaxis se parece mucho a
escribir un método normal, los tipos de argumento y el tipo de valor devuelto ya forman parte de la declaración
de función.
Asignación definitiva
Las expresiones lambda son objetos que se declaran y se asignan en tiempo de ejecución. Para poder usar una
expresión lambda, debe asignarse definitivamente: se debe declarar la variable Action / Func a la que se va a
asignar y luego asignar a esta la expresión lambda. Observe que LambdaFactorial debe declarar e inicializar la
expresión lambda nthFactorial antes de definirla. De no hacerlo, se produce un error en tiempo de
compilación por hacer referencia a nthFactorial antes de asignarlo.
Las funciones locales se definen en tiempo de compilación. Dado que no están asignadas a variables, se puede
hacer referencia a ellas desde cualquier ubicación del código que esté en ámbito ; en el primer ejemplo
LocalFunctionFactorial , se podría declarar la función local por encima o por debajo de la instrucción return y
no desencadenar ningún error del compilador.
Estas diferencias implican que los algoritmos recursivos son más fáciles de crear usando funciones locales.
Puede declarar y definir una función local que se llama a sí misma. Las expresiones lambda se deben declarar y
se les debe asignar un valor predeterminado para que se les pueda volver a asignar un cuerpo que haga
referencia a la misma expresión lambda.
Implementación como delegado
Las expresiones lambda se convierten en delegados en el momento en que se declaran. Las funciones locales
son más flexibles, ya que se pueden escribir como un método tradicional o como un delegado. Las funciones
locales solo se convierten en delegados cuando se usan como delegados.
Si se declara una función local y solo se hace referencia a ella llamándola como un método, no se convertirá en
un delegado.
Captura de variables
Las reglas de asignación definitiva también afectan a las variables capturadas por la función local o la expresión
lambda. El compilador puede efectuar un análisis estático que permite a las funciones locales asignar
definitivamente variables capturadas en el ámbito de inclusión. Considere este ejemplo:
int M()
{
int y;
LocalFunction();
return y;
El compilador puede determinar que LocalFunction asigna y definitivamente cuando se le llama. Como se
llama a LocalFunction antes de la instrucción return , y se asigna definitivamente en la instrucción return .
Observe que cuando una función local captura variables en el ámbito de inclusión, la función local se
implementa como un tipo delegado.
Asignaciones de montón
Dependiendo de su uso, las funciones locales pueden evitar las asignaciones de montón que siempre son
necesarias para las expresiones lambda. Si una función local no se convierte nunca en un delegado y ninguna de
las variables capturadas por la función local se captura con otras expresiones lambda o funciones locales que se
han convertido en delegados, el compilador puede evitar las asignaciones de montón.
Considere este ejemplo asincrónico:
La clausura de esta expresión lambda contiene las variables address , index y name . En el caso de las funciones
locales, el objeto que implementa el cierre puede ser un tipo struct . Luego, ese tipo de estructura se pasaría
por referencia a la función local. Esta diferencia de implementación supondría un ahorro en una asignación.
La creación de instancias necesaria para las expresiones lambda significa asignaciones de memoria adicionales,
lo que puede ser un factor de rendimiento en rutas de acceso de código crítico en el tiempo. Las funciones
locales no suponen esta sobrecarga. En el ejemplo anterior, la versión de las funciones locales tiene dos
asignaciones menos que la versión de la expresión lambda.
Si sabe que la función local no se va a convertir en delegado y ninguna de las variables capturadas por ella han
sido capturadas por otras expresiones lambda o funciones locales que se han convertido en delegados, puede
garantizar la no asignación de la función local al montón al declararla como función local static . Observe que
esta característica está disponible en C# 8.0 y versiones más recientes.
NOTE
La función local equivalente de este método también usa una clase para el cierre. Si el cierre de una función local se
implementa como class o struct es un detalle de implementación. Una función local puede usar struct mientras
una expresión lambda siempre usará class .
Una ventaja final que no se muestra en este ejemplo es que las funciones locales pueden implementarse como
iteradores, con la sintaxis yield return para producir una secuencia de valores.
return LowercaseIterator();
IEnumerable<string> LowercaseIterator()
{
foreach (var output in input.Select(item => item.ToLower()))
{
yield return output;
}
}
}
La instrucción yield return no se permite en las expresiones lambda; vea el Error del compilador CS1621.
Aunque las funciones locales pueden parecer redundantes con respecto a las expresiones lambda, en realidad
tienen finalidades y usos diferentes. Las funciones locales son más eficaces si se quiere escribir una función a la
que solo se llame desde el contexto de otro método.
Vea también
Métodos
Valores devueltos y variables locales de tipo ref
16/09/2021 • 7 minutes to read
A partir de C# 7.0, C# admite los valores devueltos de referencia (valores devueltos de tipo ref). Un valor
devuelto de referencia permite que un método devuelva una referencia a una variable, en lugar de un valor, al
autor de una llamada. El autor de la llamada puede tratar la variable devuelta como si se hubiera devuelto por
valor o por referencia. El autor de la llamada puede crear una variable que sea una referencia al valor devuelto,
lo que se conoce como una referencia local.
Una asignación por valor lee el valor de una variable y lo asigna a una nueva variable:
La asignación anterior declara p como una variable local. Su valor inicial se copia a partir de la lectura del valor
devuelto por GetContactInformation . Las asignaciones futuras a p no cambiarán el valor de la variable devuelta
por GetContactInformation . La variable p ya no es un alias de la variable devuelta.
Se declara una variable local tipo ref para copiar el alias en el valor original. En la siguiente asignación, p es un
alias para la variable devuelta desde GetContactInformation .
El uso posterior de p es lo mismo que usar la variable devuelta por GetContactInformation , porque p es un
alias para dicha variable. Los cambios realizados en p también modifican la variable devuelta desde
GetContactInformation .
La palabra clave ref se usa antes de la declaración de variable local y antes de la llamada al método.
Puede acceder a un valor por referencia de la misma manera. En algunos casos, acceder a un valor por
referencia aumenta el rendimiento, ya que evita una operación de copia potencialmente cara. Por ejemplo, en la
instrucción siguiente se muestra cómo es posible definir un valor local de referencia que se usa para hacer
referencia a un valor.
ref VeryLargeStruct reflocal = ref veryLargeStruct;
La palabra clave ref se usa antes de la declaración de variable local y antes del valor en el segundo ejemplo. Si
no se incluyen ambas palabras clave ref en la asignación y declaración de variable en ambos ejemplos, se
produce el error del compilador CS8172, "No se puede inicializar una variable por referencia con un valor".
Antes de C# 7.3, las variables locales de tipo ref no se podían reasignar para hacer referencia a otro
almacenamiento después de haberse inicializado. Esta restricción se ha quitado. En el ejemplo siguiente, se
muestra una reasignación:
Las variables locales de tipo ref todavía deben inicializarse cuando se declaran.
using System;
class NumberStore
{
int[] numbers = { 1, 3, 7, 15, 31, 63, 127, 255, 511, 1023 };
En el ejemplo siguiente, se llama al método NumberStore.FindNumber para recuperar el primer valor que es
mayor o igual que 16. Después, el autor de la llamada duplica el valor devuelto por el método. En el resultado
del ejemplo se muestra el cambio reflejado en el valor de los elementos de matriz de la instancia NumberStore .
Sin que se admitan los valores devueltos de referencia, este tipo de operación se realiza al devolver el índice del
elemento de matriz junto con su valor. Después, el autor de la llamada puede usar este índice para modificar el
valor en una llamada al método independiente. En cambio, el autor de la llamada también puede modificar el
índice para tener acceso a otros valores de matriz y, posiblemente, modificarlos.
En el siguiente ejemplo, se muestra cómo el método FindNumber podría modificarse después de C# 7.3 para
usar la reasignación local de tipo ref:
using System;
class NumberStore
{
int[] numbers = { 1, 3, 7, 15, 31, 63, 127, 255, 511, 1023 };
Esta segunda versión es más eficaz con secuencias más largas en escenarios donde el número buscado está más
cerca del final de la matriz, ya que en la matriz se itera desde el final hacia el principio, lo que hace que se
examinen menos elementos.
Vea también
ref (palabra clave)
Escritura de código seguro y eficaz
Pasar parámetros (Guía de programación de C#)
16/09/2021 • 2 minutes to read
En C#, los argumentos se pueden pasar a parámetros por valor o por referencia. El paso de parámetros por
referencia permite a los miembros de funciones, métodos, propiedades, indexadores, operadores y
constructores cambiar el valor de los parámetros y hacer que ese cambio persista en el entorno de la llamada.
Para pasar un parámetro por referencia con la intención de cambiar el valor, use la palabra clave ref o out .
Para pasar un parámetro por referencia con la intención de evitar la copia pero no modificar el valor, use el
modificador in . En los ejemplos de este tema, para simplificar, solo se usa la palabra clave ref . Para obtener
más información sobre la diferencia entre in , ref y out , vea in, ref y out.
En el ejemplo siguiente se muestra la diferencia entre los parámetros de valor y de referencia.
class Program
{
static void Main(string[] args)
{
int arg;
// Passing by value.
// The value of arg in Main is not changed.
arg = 4;
squareVal(arg);
Console.WriteLine(arg);
// Output: 4
// Passing by reference.
// The value of arg in Main is changed.
arg = 4;
squareRef(ref arg);
Console.WriteLine(arg);
// Output: 16
}
// Passing by reference
static void squareRef(ref int refParameter)
{
refParameter *= refParameter;
}
}
Una variable tipo de valor contiene sus datos directamente, en oposición a la variable tipo de referencia, que
contiene una referencia a sus datos. Pasar una variable tipo de valor a un método en función del valor significa
pasar una copia de la variable al método. Ningún cambio realizado en el parámetro dentro del método afecta a
los datos originales almacenados en la variable de argumentos. Si desea que el método al que se llama cambie
el valor del argumento, debe pasarlo en función de la referencia, con la palabra clave ref o out. También puede
usar la palabra clave in para pasar un parámetro de valor por referencia para evitar la copia, a la vez que se
asegura de que el valor no se modificará. Para simplificar, en el ejemplo siguiente se usa ref .
class PassingValByVal
{
static void SquareIt(int x)
// The parameter x is passed by value.
// Changes to x will not affect the original value of x.
{
x *= x;
System.Console.WriteLine("The value inside the method: {0}", x);
}
static void Main()
{
int n = 5;
System.Console.WriteLine("The value before calling the method: {0}", n);
La variable n es un tipo de valor. Contiene sus datos, el valor 5 . Cuando se invoca SquareIt , el contenido de
n se copia en el parámetro x , que se multiplica dentro del método. En Main , sin embargo, el valor de n es el
mismo después de llamar al método SquareIt que el que era antes. El cambio que tiene lugar dentro del
método solo afecta a la variable local x .
class PassingValByRef
{
static void SquareIt(ref int x)
// The parameter x is passed by reference.
// Changes to x will affect the original value of x.
{
x *= x;
System.Console.WriteLine("The value inside the method: {0}", x);
}
static void Main()
{
int n = 5;
System.Console.WriteLine("The value before calling the method: {0}", n);
En este ejemplo, no es el valor de n el que se pasa, sino una referencia a n . El parámetro x no es un entero;
se trata de una referencia a int , en este caso, una referencia a n . Por tanto, cuando x se multiplica dentro del
método, lo que realmente se multiplica es a lo que x hace referencia, n .
Al llamar al método SwapByRef , use la palabra clave ref en la llamada, como se muestra en el ejemplo
siguiente.
static void Main()
{
int i = 2, j = 3;
System.Console.WriteLine("i = {0} j = {1}" , i, j);
Vea también
Guía de programación de C#
Pasar parámetros
Pasar parámetros Reference-Type
Pasar parámetros Reference-Type (Guía de
programación de C#)
16/09/2021 • 4 minutes to read
Una variable de un tipo de referencia no contiene sus datos directamente, contiene una referencia a sus datos. Al
pasar un parámetro de tipo de referencia por valor, es posible cambiar los datos que pertenecen al objeto al que
se hace referencia, como el valor de un miembro de clase. En cambio, no se puede cambiar el valor de la propia
referencia; por ejemplo, no puede usar la misma referencia para asignar memoria para un nuevo objeto y hacer
que persista fuera del método. Para ello, pase el parámetro mediante las palabras clave ref u out. Para
simplificar, en el ejemplo siguiente se usa ref .
class PassingRefByVal
{
static void Change(int[] pArray)
{
pArray[0] = 888; // This change affects the original element.
pArray = new int[5] {-3, -1, -2, -3, -4}; // This change is local.
System.Console.WriteLine("Inside the method, the first element is: {0}", pArray[0]);
}
Change(arr);
System.Console.WriteLine("Inside Main, after calling the method, the first element is: {0}", arr
[0]);
}
}
/* Output:
Inside Main, before calling the method, the first element is: 1
Inside the method, the first element is: -3
Inside Main, after calling the method, the first element is: 888
*/
En el ejemplo anterior, la matriz, arr , que es un tipo de referencia, se pasa al método sin el parámetro ref . En
tal caso, se pasa al método una copia de la referencia, que apunta a arr . El resultado muestra que es posible
que el método cambie el contenido de un elemento de matriz, en este caso de 1 a 888 . En cambio, si se asigna
una nueva porción de memoria al usar el operador new dentro del método Change , la variable pArray hace
referencia a una nueva matriz. Por tanto, cualquier cambio que hubiese después no afectará a la matriz original,
arr , que se ha creado dentro de Main . De hecho, se crean dos matrices en este ejemplo, una dentro de Main y
otra dentro del método Change .
Pasar tipos de referencia en función de la referencia
El ejemplo siguiente es el mismo que el anterior, salvo que la palabra clave ref se agrega a la llamada y al
encabezado de método. Los cambios que tengan lugar en el método afectan a la variable original en el
programa que realiza la llamada.
class PassingRefByRef
{
static void Change(ref int[] pArray)
{
// Both of the following changes will affect the original variables:
pArray[0] = 888;
pArray = new int[5] {-3, -1, -2, -3, -4};
System.Console.WriteLine("Inside the method, the first element is: {0}", pArray[0]);
}
Change(ref arr);
System.Console.WriteLine("Inside Main, after calling the method, the first element is: {0}",
arr[0]);
}
}
/* Output:
Inside Main, before calling the method, the first element is: 1
Inside the method, the first element is: -3
Inside Main, after calling the method, the first element is: -3
*/
Todos los cambios que tienen lugar dentro del método afectan a la matriz original en Main . De hecho, la matriz
original se reasigna mediante el operador new . Por tanto, después de llamar al método Change , cualquier
referencia a arr apunta a la matriz de cinco elementos, que se crea en el método Change .
En este ejemplo, los parámetros tienen que pasarse en función de la referencia para afectar a las variables en el
programa que realiza la llamada. Si quita la palabra clave ref tanto del encabezado de método como de la
llamada al método, no se llevará a cabo ningún cambio en el programa que realiza la llamada.
Para obtener más información sobre las cadenas, vea string.
Vea también
Guía de programación de C#
Pasar parámetros
ref
in
out
Tipos de referencia
Procedimiento para saber las diferencias entre pasar
a un método un struct y una referencia a clase (Guía
de programación de C#)
16/09/2021 • 2 minutes to read
En el ejemplo siguiente se muestra cómo pasar un struct a un método es diferente de pasar una instancia de
clase a un método. En el ejemplo, los dos argumentos (struct e instancia de clase) se pasan mediante valor, y
ambos métodos cambian el valor de un campo del argumento. En cambio, los resultados de los dos métodos no
son los mismos porque lo que se pasa cuando pasa un struct difiere de lo que se pasa cuando pasa una
instancia de una clase.
Como un struct es un tipo de valor, cuando pasa un struct mediante valor a un método, el método recibe y
funciona en una copia del argumento struct. El método no tiene acceso al struct original en el método de
llamada y, por lo tanto, no puede cambiarlo de ninguna manera. El método solo puede cambiar la copia.
Una instancia de clase es un tipo de referencia, no un tipo de valor. Cuando un tipo de referencia se pasa
mediante valor a un método, el método recibe una copia de la referencia a la instancia de clase. Es decir, el
método al que se llama recibe una copia de la dirección de la instancia y el método de llamada conserva la
dirección original de la instancia. La instancia de clase en el método de llamada tiene una dirección, el parámetro
en el método que se ha llamado tiene una copia de la dirección, y ambas direcciones hacen referencia al mismo
objeto. Como el parámetro solo contiene una copia de la dirección, el método al que se ha llamado no puede
cambiar la dirección de la instancia de clase en el método de llamada. En cambio, el método al que se ha
llamado puede usar la copia de la dirección para acceder a los miembros de clase a los que hacen referencia la
dirección original y la copia de la dirección. Si el método al que se ha llamado cambia un miembro de clase, la
instancia de clase original en el método de llamada también cambia.
En el resultado del ejemplo siguiente se ilustra la diferencia. El valor del campo willIChange de la instancia de
clase se cambia mediante la llamada al método ClassTaker porque el método usa la dirección en el parámetro
para buscar el campo especificado de la instancia de clase. El campo willIChange del struct en el método de
llamada no se cambia mediante la llamada al método StructTaker porque el valor del argumento es una copia
del propio struct, no una copia de su dirección. StructTaker cambia la copia, y la copia se pierde cuando se
completa la llamada a StructTaker .
Ejemplo
using System;
class TheClass
{
public string willIChange;
}
struct TheStruct
{
public string willIChange;
}
class TestClassAndStruct
{
static void ClassTaker(TheClass c)
{
c.willIChange = "Changed";
}
ClassTaker(testClass);
StructTaker(testStruct);
Consulte también
Guía de programación de C#
Clases
Tipos de estructura
Pasar parámetros
Variables locales con asignación implícita de tipos
(Guía de programación de C#)
16/09/2021 • 5 minutes to read
Las variables locales pueden declararse sin proporcionar un tipo explícito. La palabra clave var indica al
compilador que infiera el tipo de la variable a partir de la expresión de la derecha de la instrucción de
inicialización. El tipo inferido puede ser un tipo integrado, un tipo anónimo, un tipo definido por el usuario o un
tipo definido en la biblioteca de clases .NET. Para obtener más información sobre cómo inicializar las matrices
con var , vea Matrices con tipo implícito.
Los ejemplos siguientes muestran distintas formas en que se pueden declarar variables locales con var :
// i is compiled as an int
var i = 5;
// s is compiled as a string
var s = "Hello";
// a is compiled as int[]
var a = new[] { 0, 1, 2 };
Es importante comprender que la palabra clave var no significa "variant" ni indica que la variable esté
débilmente tipada o esté enlazada en tiempo de ejecución. Solo significa que el compilador determina y asigna
el tipo más adecuado.
La palabra clave var se puede usar en los contextos siguientes:
En variables locales (variables declaradas en el ámbito del método), tal y como se muestra en el ejemplo
anterior.
En una instrucción de inicialización for.
Para obtener más información, vea Procedimiento para usar matrices y variables locales con tipo implícito en
expresiones de consulta.
class ImplicitlyTypedLocals2
{
static void Main()
{
string[] words = { "aPPLE", "BlUeBeRrY", "cHeRry" };
Comentarios
Las siguientes restricciones se aplican a las declaraciones de variable con tipo implícito:
var solo se puede usar cuando una variable local se declara e inicializa en la misma instrucción; la
variable no se puede inicializar en null ni en un grupo de métodos o una función anónima.
var no se puede usar en campos en el ámbito de clase.
Las variables declaradas con var no se pueden usar en la expresión de inicialización. En otras palabras,
esta expresión es válida: int i = (i = 20); , pero esta expresión genera un error en tiempo de
compilación: var i = (i = 20);
No se pueden inicializar varias variables con tipo implícito en la misma instrucción.
Si en el ámbito se encuentra un tipo con nombre var , la palabra clave var se resolverá en ese nombre
de tipo y no se tratará como parte de una declaración de variable local con tipo implícito.
El establecimiento de tipos implícitos con la palabra clave var solo puede aplicarse a variables en el ámbito del
método local. El establecimiento de tipos implícitos no está disponible para los campos de clase ya que el
compilador C# encontraría una paradoja lógica al procesar el código: el compilador necesita conocer el tipo de
campo, pero no puede determinar el tipo hasta que se analiza la expresión de asignación, y no se puede evaluar
la expresión sin conocer el tipo. Observe el código siguiente:
bookTitles es un campo de clase dado el tipo var . Como el campo no tiene ninguna expresión que evaluar, es
imposible que el compilador pueda inferir el tipo bookTitles que se supone que es. Además, agregar una
expresión al campo (como se haría con una variable local) también es suficiente:
Cuando el compilador encuentra campos durante la compilación de código, registra el tipo de cada campo antes
de procesar las expresiones asociadas a él. El compilador encuentra la misma paradoja intentando analizar
bookTitles : necesita saber el tipo de campo, pero el compilador normalmente determinaría el tipo de var
analizando la expresión, lo que no es posible sin conocer de antemano el tipo.
Es posible que var también pueda resultar útil con expresiones de consulta en las que es difícil determinar el
tipo construido exacto de la variable de consulta. Esto puede ocurrir con las operaciones de agrupamiento y
ordenación.
La palabra clave var también puede ser útil cuando resulte tedioso escribir el tipo específico de la variable en el
teclado, o sea obvio o no aumente la legibilidad del código. Un ejemplo en el que var resulta útil de esta
manera es cuando se usa con tipos genéricos anidados, como los que se emplean con las operaciones de grupo.
En la siguiente consulta, el tipo de la variable de consulta es IEnumerable<IGrouping<string, Student>> . Siempre
que usted y otras personas que deban mantener el código comprendan esto, no hay ningún problema con el
uso de tipos implícitos por comodidad y brevedad.
// Same as previous example except we use the entire last name as a key.
// Query variable is an IEnumerable<IGrouping<string, Student>>
var studentQuery3 =
from student in students
group student by student.Last;
El uso de var ayuda a simplificar el código, pero debe quedar restringido a los casos en los que sea necesario,
o cuando haga que el código sea más fácil de leer. Para obtener más información sobre cuándo usar var
correctamente, vea la sección Variables locales con asignación implícita de tipos en el artículo sobre directrices
de codificación de C#.
Vea también
Referencia de C#
Matrices con tipo implícito
Procedimiento para usar matrices y variables locales con tipo implícito en expresiones de consulta
Tipos anónimos
Inicializadores de objeto y colección
var
LINQ en C#
LINQ (Language Integrated Query)
Instrucciones de iteración
using (instrucción)
Procedimiento para usar matrices y variables locales
con tipo implícito en expresiones de consulta (Guía
de programación de C#)
16/09/2021 • 2 minutes to read
Puede usar variables locales con tipo implícito siempre que quiera que el compilador determine el tipo de una
variable local. Debe usar variables locales con tipo implícito para almacenar tipos anónimos, que a menudo se
usan en las expresiones de consulta. En los ejemplos siguientes, se muestran los usos obligatorios y opcionales
de las variables locales con tipo implícito en las consultas.
Las variables locales con tipo implícito se declaran mediante la palabra clave contextual var. Para obtener más
información, vea Variables locales con asignación implícita de tipos y Matrices con asignación implícita de tipos.
Ejemplos
En el ejemplo siguiente, se muestra un escenario común en el que la palabra clave var es necesaria: una
expresión de consulta que genera una secuencia de tipos anónimos. En este escenario, la variable de consulta y
la variable de iteración en la instrucción foreach deben escribirse de forma implícita mediante el uso de var
porque no se tiene acceso a un nombre de tipo para el tipo anónimo. Para obtener más información sobre los
tipos anónimos, vea Tipos anónimos.
En el ejemplo siguiente, se usa la palabra clave var en una situación similar, pero en la que el uso de var es
opcional. Dado que student.LastName es una cadena, la ejecución de la consulta devuelve una secuencia de
cadenas. Por tanto, el tipo de queryID podría declararse como System.Collections.Generic.IEnumerable<string>
en lugar de var . La palabra clave var se usa por comodidad. En el ejemplo, la variable de iteración en la
instrucción foreach se escribe de forma explícita como una cadena, pero se podría declarar mediante var .
Dado que el tipo de la variable de iteración no es un tipo anónimo, el uso de var es opcional, no es obligatorio.
Recuerde que var no es un tipo, sino una instrucción para que el compilador deduzca y asigne el tipo.
// Variable queryId could be declared by using
// System.Collections.Generic.IEnumerable<string>
// instead of var.
var queryId =
from student in students
where student.Id > 111
select student.LastName;
Vea también
Guía de programación de C#
Métodos de extensión
LINQ (Language Integrated Query)
var
LINQ en C#
Métodos de extensión (Guía de programación de
C#)
16/09/2021 • 10 minutes to read
Los métodos de extensión permiten "agregar" métodos a los tipos existentes sin crear un nuevo tipo derivado,
recompilar o modificar de otra manera el tipo original. Los métodos de extensión son métodos estáticos, pero se
les llama como si fueran métodos de instancia en el tipo extendido. En el caso del código de cliente escrito en
C#, F# y Visual Basic, no existe ninguna diferencia aparente entre llamar a un método de extensión y llamar a los
métodos definidos en un tipo.
Los métodos de extensión más comunes son los operadores de consulta LINQ estándar, que agregan funciones
de consulta a los tipos System.Collections.IEnumerable y System.Collections.Generic.IEnumerable<T> existentes.
Para usar los operadores de consulta estándar, inclúyalos primero en el ámbito con una directiva
using System.Linq . A partir de ese momento, cualquier tipo que implemente IEnumerable<T> parecerá tener
métodos de instancia como GroupBy, OrderBy, Average, etc. Puede ver estos métodos adicionales en la
finalización de instrucciones de IntelliSense al escribir "punto" después de una instancia de un tipo
IEnumerable<T>, como List<T> o Array.
Ejemplo de OrderBy
En el ejemplo siguiente se muestra cómo llamar al método OrderBy de operador de consulta estándar en una
matriz de enteros. La expresión entre paréntesis es una expresión lambda. Muchos operadores de consulta
estándar toman expresiones lambda como parámetros, pero no es un requisito para los métodos de extensión.
Para obtener más información, vea Expresiones lambda.
class ExtensionMethods2
{
Los métodos de extensión se definen como métodos estáticos, pero se les llama usando la sintaxis de método
de instancia. Su primer parámetro especifica en qué tipo funciona el método. El parámetro va precedido del
modificador this. Los métodos de extensión únicamente se encuentran dentro del ámbito cuando el espacio de
nombres se importa explícitamente en el código fuente con una directiva using .
En el ejemplo siguiente se muestra un método de extensión definido para la clase System.String. Se define
dentro de una clase estática no anidada y no genérica:
namespace ExtensionMethods
{
public static class MyExtensions
{
public static int WordCount(this String str)
{
return str.Split(new char[] { ' ', '.', '?' },
StringSplitOptions.RemoveEmptyEntries).Length;
}
}
}
El método de extensión WordCount se puede incluir en el ámbito con esta directiva using :
using ExtensionMethods;
El método de extensión se invoca en el código con la sintaxis de método de instancia. El lenguaje intermedio (IL)
generado por el compilador convierte el código en una llamada en el método estático. El principio de
encapsulación no se infringe realmente. Los métodos de extensión no pueden tener acceso a las variables
privadas en el tipo que extienden.
Para obtener más información, vea Procedimiento para implementar e invocar un método de extensión
personalizado.
En general, probablemente se llamará a métodos de extensión con mucha más frecuencia de la que se
implementarán métodos propios. Dado que los métodos de extensión se llaman con la sintaxis de método de
instancia, no se requieren conocimientos especiales para usarlos desde el código de cliente. Para habilitar los
métodos de extensión para un tipo determinado, basta con agregar una directiva using para el espacio de
nombres en el que se definen los métodos. Por ejemplo, para usar los operadores de consulta estándar, agregue
esta directiva using al código:
using System.Linq;
(Puede que haya que agregar también una referencia a System.Core.dll). Observará que los operadores de
consulta estándar aparecen ahora en IntelliSense como métodos adicionales disponibles para la mayoría de los
tipos IEnumerable<T>.
// Define three classes that implement IMyInterface, and then use them to test
// the extension methods.
namespace ExtensionMethodsDemo1
{
using System;
using Extensions;
using DefineIMyInterface;
using DefineIMyInterface;
class A : IMyInterface
{
public void MethodB() { Console.WriteLine("A.MethodB()"); }
}
class B : IMyInterface
{
public void MethodB() { Console.WriteLine("B.MethodB()"); }
public void MethodA(int i) { Console.WriteLine("B.MethodA(int i)"); }
}
class C : IMyInterface
{
public void MethodB() { Console.WriteLine("C.MethodB()"); }
public void MethodA(object obj)
{
Console.WriteLine("C.MethodA(object obj)");
}
}
class ExtMethodDemo
{
static void Main(string[] args)
{
// Declare an instance of class A, class B, and class C.
A a = new A();
B b = new B();
C c = new C();
Vea también
Guía de programación de C#
Ejemplos de programación en paralelo (incluyen numerosos métodos de extensión de ejemplo)
Expresiones lambda
Información general sobre operadores de consulta estándar
Conversion rules for Instance parameters and their impact (Reglas de conversión para los parámetros de
instancia y su impacto)
Extension methods Interoperability between languages (Interoperabilidad de los métodos de extensión entre
lenguajes)
Extension methods and Curried Delegates (Métodos de extensión y delegados currificados)
Extension method Binding and Error reporting (Enlazar métodos de extensión y notificación de errores)
Procedimiento para implementar e invocar un
método de extensión personalizado (Guía de
programación de C#)
16/09/2021 • 2 minutes to read
En este tema se muestra cómo implementar sus propios métodos de extensión para cualquier tipo de .NET. El
código de cliente puede usar los métodos de extensión agregando una referencia a la DLL que los contiene y
agregando una directiva using que especifique el espacio de nombres en el que se definen los métodos de
extensión.
Ejemplo
En el ejemplo siguiente se implementa un método de extensión denominado WordCount en la clase
CustomExtensions.StringExtension . El método opera en la clase String, que se especifica como el primer
parámetro de método. El espacio de nombres CustomExtensions se importa en el espacio de nombres de la
aplicación y se llama al método dentro del método Main .
using System.Linq;
using System.Text;
using System;
namespace CustomExtensions
{
// Extension methods must be defined in a static class.
public static class StringExtension
{
// This is the extension method.
// The first parameter takes the "this" modifier
// and specifies the type for which the method is defined.
public static int WordCount(this String str)
{
return str.Split(new char[] {' ', '.','?'}, StringSplitOptions.RemoveEmptyEntries).Length;
}
}
}
namespace Extension_Methods_Simple
{
// Import the extension method namespace.
using CustomExtensions;
class Program
{
static void Main(string[] args)
{
string s = "The quick brown fox jumped over the lazy dog.";
// Call the method as if it were an
// instance method on the type. Note that the first
// parameter is not specified by the calling code.
int i = s.WordCount();
System.Console.WriteLine("Word count of s is {0}", i);
}
}
}
Seguridad de .NET
Los métodos de extensión no presentan ninguna vulnerabilidad de seguridad específica. No se pueden usar
nunca para suplantar los métodos existentes en un tipo, porque todos los conflictos de nombres se resuelven a
favor de la instancia o del método estático definido por el tipo en cuestión. Los métodos de extensión no pueden
tener acceso a los datos privados de la clase extendida.
Vea también
Guía de programación de C#
Métodos de extensión
LINQ (Language Integrated Query)
Clases estáticas y sus miembros
protected
internal
public
this
namespace
Procedimiento para crear un método nuevo para
una enumeración (Guía de programación de C#)
16/09/2021 • 2 minutes to read
Puede usar métodos de extensión para agregar funcionalidad a un tipo de enumeración concreto.
Ejemplo
En el ejemplo siguiente, la enumeración Grades representa las posibles calificaciones con letras que un alumno
puede recibir en una clase. Un método de extensión denominado Passing se agrega al tipo Grades para que
cada instancia de ese tipo "sepa" ahora si representa una calificación de aprobado o no.
using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
namespace EnumExtension
{
// Define an extension method in a non-nested static class.
public static class Extensions
{
public static Grades minPassing = Grades.D;
public static bool Passing(this Grades grade)
{
return grade >= minPassing;
}
}
Extensions.minPassing = Grades.C;
Console.WriteLine("\r\nRaising the bar!\r\n");
Console.WriteLine("First {0} a passing grade.", g1.Passing() ? "is" : "is not");
Console.WriteLine("Second {0} a passing grade.", g2.Passing() ? "is" : "is not");
}
}
}
/* Output:
First is a passing grade.
Second is not a passing grade.
Tenga en cuenta que la clase Extensions también contiene una variable estática que se actualiza dinámicamente
y que el valor devuelto del método de extensión refleja el valor actual de esa variable. Esto demuestra que, en
segundo plano, los métodos de extensión se invocan directamente en la clase estática en donde se definen.
Vea también
Guía de programación de C#
Métodos de extensión
Argumentos opcionales y con nombre (Guía de
programación de C#)
16/09/2021 • 8 minutes to read
C# 4 introduce argumentos opcionales y con nombre. Los argumentos con nombre permiten especificar un
argumento para un parámetro asociando el argumento a su nombre y no a su posición en la lista de
parámetros. Los argumentos opcionales permiten omitir argumentos para algunos parámetros. Ambas técnicas
se pueden usar con métodos, indexadores, constructores y delegados.
Cuando se usan argumentos opcionales y con nombre, los argumentos se evalúan por el orden en que aparecen
en la lista de argumentos, no en la lista de parámetros.
Los parámetros opcionales y con nombre permiten proporcionar argumentos para los parámetros
seleccionados. Esta funcionalidad facilita enormemente las llamadas a interfaces COM, como las API de
automatización de Microsoft Office.
Si no recuerda el orden de los parámetros pero conoce sus nombres, puede enviar los argumentos en cualquier
orden.
Los argumentos con nombre también mejoran la legibilidad del código al identificar lo que cada argumento
representa. En el método de ejemplo siguiente, sellerName no puede ser nulo ni un espacio en blanco. Como
sellerName y productName son tipos de cadena, en lugar de enviar argumentos por posición, tiene sentido usar
argumentos con nombre para eliminar la ambigüedad entre ambos y evitar confusiones para aquellos que lean
el código.
Los argumentos con nombre, cuando se usan con argumentos posicionales, son válidos siempre que
no vayan seguidos de ningún argumento posicional o,
a partir de C# 7.2, se usen en la posición correcta. En este ejemplo, el parámetro orderNum está en la
posición correcta pero no se le asigna un nombre de manera explícita.
PrintOrderDetails(sellerName: "Gift Shop", 31, productName: "Red Mug");
Los argumentos posicionales que siguen a los argumentos con nombre desordenados no son válidos.
// This generates CS1738: Named argument specifications must appear after all fixed arguments have been
specified.
PrintOrderDetails(productName: "Red Mug", 31, "Gift Shop");
Ejemplo
Con este código se implementan los ejemplos de esta sección junto con otros adicionales.
class NamedExample
{
static void Main(string[] args)
{
// The method can be called in the normal way, by using positional arguments.
PrintOrderDetails("Gift Shop", 31, "Red Mug");
Argumentos opcionales
La definición de un método, constructor, indexador o delegado puede especificar que sus parámetros son
necesarios u opcionales. Todas las llamadas deben proporcionar argumentos para todos los parámetros
necesarios, pero pueden omitir los argumentos para los parámetros opcionales.
Cada parámetro opcional tiene un valor predeterminado como parte de su definición. Si no se envía ningún
argumento para ese parámetro, se usa el valor predeterminado. Un valor predeterminado debe ser uno de los
siguientes tipos de expresiones:
una expresión constante;
una expresión con el formato new ValType() , donde ValType es un tipo de valor, como enum o struct;
una expresión con el formato default(ValType), donde ValType es un tipo de valor.
Los parámetros opcionales se definen al final de la lista de parámetros después de los parámetros necesarios. Si
el autor de la llamada proporciona un argumento para algún parámetro de una sucesión de parámetros
opcionales, debe proporcionar argumentos para todos los parámetros opcionales anteriores. No se admiten
espacios separados por comas en la lista de argumentos. Por ejemplo, en el código siguiente, el método de
instancia ExampleMethod se define con un parámetro necesario y dos opcionales.
La llamada siguiente a ExampleMethod genera un error del compilador, porque se proporciona un argumento
para el tercer parámetro pero no para el segundo.
//anExample.ExampleMethod(3, ,4);
Pero si conoce el nombre del tercer parámetro, puede usar un argumento con nombre para realizar la tarea.
En IntelliSense los corchetes se usan para indicar parámetros opcionales, como se muestra en la ilustración
siguiente:
NOTE
También puede declarar parámetros opcionales con la clase OptionalAttribute de .NET. Los parámetros
OptionalAttribute no requieren un valor predeterminado.
Ejemplo
En el ejemplo siguiente, el constructor de ExampleClass tiene un solo parámetro, que es opcional. El método de
instancia ExampleMethod tiene un parámetro necesario, required , y dos parámetros opcionales, optionalstr y
optionalint . El código de Main muestra las distintas formas en que se pueden invocar el constructor y el
método.
namespace OptionalNamespace
{
class OptionalExample
{
static void Main(string[] args)
{
// Instance anExample does not send an argument for the constructor's
// optional parameter.
ExampleClass anExample = new ExampleClass();
anExample.ExampleMethod(1, "One", 1);
anExample.ExampleMethod(2, "Two");
anExample.ExampleMethod(3);
class ExampleClass
{
private string _name;
En el código anterior se muestran varios ejemplos en los que los parámetros opcionales no se aplican
correctamente. En primer lugar, se muestra que se debe proporcionar un argumento para el primer parámetro,
que es obligatorio.
Interfaces COM
Los argumentos opcionales y con nombre, además de compatibilidad con objetos dinámicos, mejoran
considerablemente la interoperabilidad con las API de COM, como las API de automatización de Office.
Por ejemplo, el método AutoFormat de la interfaz Range de Microsoft Office Excel tiene siete parámetros, todos
ellos opcionales. Estos parámetros se muestran en la ilustración siguiente:
En C# 3.0 y versiones anteriores, se requiere un argumento para cada parámetro, tal y como se muestra en el
ejemplo siguiente.
var myFormat =
Microsoft.Office.Interop.Excel.XlRangeAutoFormat.xlRangeAutoFormatAccounting1;
Pero la llamada a AutoFormat se puede simplificar considerablemente mediante argumentos opcionales y con
nombre, que se introducen en C# 4.0. Los parámetros opcionales y con nombre permiten omitir el argumento
de un parámetro opcional si no se quiere cambiar el valor predeterminado del parámetro. En la siguiente
llamada, solo se especifica un valor para uno de los siete parámetros.
// The following code shows the same call to AutoFormat in C# 4.0. Only
// the argument for which you want to provide a specific value is listed.
excelApp.Range["A1", "B4"].AutoFormat( Format: myFormat );
Para obtener más información y ejemplos, vea Procedimiento para usar argumentos opcionales y con nombre
en la programación de Office (Guía de programación de C#) y Procedimiento para tener acceso a objetos de
interoperabilidad de Office mediante las características de Visual C# (Guía de programación de C#).
Resolución de sobrecarga
El uso de argumentos opcionales y con nombre afecta a la resolución de sobrecarga de las maneras siguientes:
Un método, indexador o constructor es un candidato para la ejecución si cada uno de sus parámetros es
opcional o corresponde, por nombre o por posición, a un solo argumento de la instrucción de llamada y el
argumento se puede convertir al tipo del parámetro.
Si se encuentra más de un candidato, se aplican las reglas de resolución de sobrecarga de las conversiones
preferidas a los argumentos que se especifican explícitamente. Los argumentos omitidos en parámetros
opcionales se ignoran.
Si dos candidatos se consideran igualmente correctos, la preferencia pasa a un candidato que no tenga
parámetros opcionales cuyos argumentos se hayan omitido en la llamada. Generalmente, la resolución de
sobrecarga prefiere aquellos candidatos que tengan menos parámetros.
Los argumentos con nombre y los argumentos opcionales, introducidos en C# 4, mejoran la comodidad, la
flexibilidad y la legibilidad en la programación de C#. Además, estas características facilitan enormemente el
acceso a interfaces COM, como las API de automatización de Microsoft Office.
En el ejemplo siguiente, el método ConvertToTable tiene dieciséis parámetros que representan las características
de una tabla, como el número de columnas y filas, el formato, los bordes, las fuentes y los colores. Los dieciséis
parámetros son opcionales, ya que la mayoría de las veces no interesa especificar valores concretos para todos
ellos. Pero si no hay argumentos opcionales y con nombre, es necesario proporcionar un valor o un valor de
marcador de posición para cada parámetro. Con los argumentos opcionales y con nombre, puede especificar
valores solamente para los parámetros necesarios para el proyecto.
Debe tener Microsoft Office Word instalado en el equipo para completar estos procedimientos.
NOTE
Es posible que el equipo muestre nombres o ubicaciones diferentes para algunos de los elementos de la interfaz de
usuario de Visual Studio en las siguientes instrucciones. La edición de Visual Studio que se tenga y la configuración que se
utilice determinan estos elementos. Para obtener más información, vea Personalizar el IDE.
2. Agregue el código siguiente al final del método para definir dónde se muestra texto en el documento y
qué texto se muestra:
DisplayInWord();
2. Presione CTRL+F5 para ejecutar el proyecto. Aparecerá un documento de Word con el texto especificado.
// Convert to a simple table. The table will have a single row with
// three columns.
range.ConvertToTable(Separator: ",");
2. Para especificar un formato predefinido para la tabla, reemplace la última línea de DisplayInWord por la
instrucción siguiente y escriba CTRL+F5. El formato puede ser cualquiera de las constantes
WdTableFormat.
Ejemplo
En el código siguiente se incluye el ejemplo completo:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Word = Microsoft.Office.Interop.Word;
namespace OfficeHowTo
{
class WordProgram
{
static void Main(string[] args)
{
DisplayInWord();
}
// Next, use the ConvertToTable method to put the text into a table.
// The method has 16 optional parameters. You only have to specify
// values for those you want to change.
// Convert to a simple table. The table will have a single row with
// three columns.
range.ConvertToTable(Separator: ",");
Vea también
Argumentos opcionales y con nombre
Constructores (Guía de programación de C#)
16/09/2021 • 2 minutes to read
Cada vez que se crea una clase o struct, se llama a su constructor. Una clase o struct puede tener varios
constructores que toman argumentos diferentes. Los constructores permiten al programador establecer valores
predeterminados, limitar la creación de instancias y escribir código flexible y fácil de leer. Para obtener más
información y ejemplos, vea Usar constructores y Constructores de instancias.
Si un constructor puede implementarse como una instrucción única, puede usar una definición del cuerpo de
expresión. En el ejemplo siguiente se define una clase Location cuyo constructor tiene un único parámetro de
cadena denominado name. La definición del cuerpo de expresión asigna el argumento al campo locationName .
static Adult()
{
minimumAge = 18;
}
También puede definir un constructor estático con una definición de cuerpo de expresión, como se muestra en el
ejemplo siguiente.
En esta sección
Utilizar constructores
Constructores de instancias
Constructores privados
Constructores estáticos
Escritura de un constructor de copia
Vea también
Guía de programación de C#
Clases, estructuras y registros
Finalizadores
static
Why Do Initializers Run In The Opposite Order As Constructors? Part One (¿Por qué los inicializadores se
ejecutan en orden contrario a los constructores? Parte uno)
Utilizar constructores (Guía de programación de
C#)
16/09/2021 • 4 minutes to read
Cuando se crea una class o un struct, se llama a su constructor. Los constructores tienen el mismo nombre que
la class o el struct y suelen inicializar los miembros de datos del nuevo objeto.
En el ejemplo siguiente, una clase denominada Taxi se define mediante un constructor simple. Luego, se crea
una instancia de la clase con el operador new. El constructor Taxi se invoca con el operador new
inmediatamente después de asignar memoria para el nuevo objeto.
public Taxi()
{
IsInitialized = true;
}
}
class TestTaxi
{
static void Main()
{
Taxi t = new Taxi();
Console.WriteLine(t.IsInitialized);
}
}
Un constructor que no toma ningún parámetro se denomina constructor sin parámetros. Los constructores sin
parámetros se invocan cada vez que se crea una instancia de un objeto mediante el operador new y no se
especifica ningún argumento en new . Para obtener más información, vea Instance Constructors (Constructores
de instancias [Guía de programación de C#]).
A menos que la clase sea static, las clases sin constructores tienen un constructor público sin parámetros por el
compilador de C# con el fin de habilitar la creación de instancias de clase. Para más información, vea Clases
estáticas y sus miembros.
Puede impedir que se cree una instancia de una clase convirtiendo el constructor en privado, de la manera
siguiente:
class NLog
{
// Private Constructor:
private NLog() { }
Para obtener más información, vea Private Constructors (Constructores privados [Guía de programación de
C#]).
Los constructores de tipos struct son similares a los constructores de clases, pero structs no puede contener
un constructor sin parámetros explícito porque el compilador proporciona uno automáticamente. Este
constructor inicializa cada campo del struct en los valores predeterminados. Pero este constructor sin
parámetros solo se invoca si las instancias de struct se crean con new . Por ejemplo, este código usa el
constructor sin parámetros para Int32, por lo que se tiene la certeza de que el entero se inicializa:
Si bien, el siguiente código genera un error del compilador porque no usa new y porque intenta usar un objeto
que no se ha inicializado:
int i;
Console.WriteLine(i);
También puede inicializar o asignar los objetos basados en structs (incluidos todos los tipos numéricos
integrados) y luego usarlos como en el ejemplo siguiente:
Por lo que no es necesario llamar al constructor sin parámetros para un tipo de valor.
Tanto las clases como los structs pueden definir constructores que toman parámetros. Los constructores que
toman parámetros deben llamarse mediante una instrucción new o base. Las clases y structs también pueden
definir varios constructores y no es necesario definir un constructor sin parámetros. Por ejemplo:
public Employee() { }
Un constructor puede usar la palabra clave base para llamar al constructor de una clase base. Por ejemplo:
public class Manager : Employee
{
public Manager(int annualSalary)
: base(annualSalary)
{
//Add further instructions here.
}
}
En este ejemplo, se llama al constructor de la clase base antes de ejecutar el bloque del constructor. La palabra
clave base puede usarse con o sin parámetros. Los parámetros del constructor se pueden usar como
parámetros en base o como parte de una expresión. Para obtener más información, vea base.
En una clase derivada, si un constructor de clase base no se llama explícitamente con la palabra clave base , se
llama implícitamente al constructor sin parámetros, si hay alguno. Esto significa que las siguientes declaraciones
del constructor son en efecto iguales:
Si una clase base no proporciona un constructor sin parámetros, la clase derivada debe realizar una llamada
explícita a un constructor base mediante base .
Un constructor puede invocar otro constructor en el mismo objeto mediante la palabra clave this. Igual que
base , this puede usarse con o sin parámetros. Además, los parámetros del constructor están disponibles
como parámetros en this o como parte de una expresión. Por ejemplo, el segundo constructor del ejemplo
anterior se puede reescribir con this :
Los constructores se pueden marcar como public, private, protected, internal, protected internal o private
protected. Estos modificadores de acceso definen cómo los usuarios de la clase pueden construir la clase. Para
obtener más información, consulte Modificadores de acceso.
Un constructor puede declararse estático mediante la palabra clave static. Los constructores estáticos se llaman
automáticamente, inmediatamente antes de acceder a los campos estáticos y, por lo general, se usan para
inicializar miembros de clase estática. Para obtener más información, vea Static Constructors (Constructores
estáticos [Guía de programación de C#]).
Vea también
Guía de programación de C#
Clases, estructuras y registros
Constructores
Finalizadores
Constructores de instancias (Guía de programación
de C#)
16/09/2021 • 4 minutes to read
Los constructores de instancias se usan para crear e inicializar las variables miembro de instancia cuando se usa
la expresión new para crear un objeto de una clase. Para inicializar una clase estática, o variables estáticas en una
clase no estática, se define un constructor estático. Para obtener más información, vea Constructores estáticos
(Guía de programación de C#).
En el siguiente ejemplo se muestra un constructor de instancias:
class Coords
{
public int x, y;
// constructor
public Coords()
{
x = 0;
y = 0;
}
}
NOTE
Para mayor claridad, esta clase contiene campos públicos. El uso de campos públicos no es una práctica de programación
recomendada porque permite que cualquier método de cualquier parte de un programa obtenga acceso sin restricciones
ni comprobaciones a los mecanismos internos de un objeto. Los miembros de datos generalmente deberían ser privados
y solo se debería tener acceso a ellos a través de las propiedades y métodos de la clase.
Se llama a este constructor de instancias cada vez que se crea un objeto basado en la clase Coords . Un
constructor como este, que no toma ningún argumento, se denomina constructor sin parámetros. Pero a
menudo resulta útil proporcionar constructores adicionales. Por ejemplo, se puede agregar un constructor a la
clase Coords que permita especificar los valores iniciales de los miembros de datos:
Esto permite crear objetos Coords con valores iniciales predeterminados o específicos, como este:
Si una clase no tiene un constructor, se genera automáticamente un constructor sin parámetros y los valores
predeterminados se usan para inicializar los campos del objeto. Por ejemplo, un int se inicializa en 0. Para
obtener información sobre los valores predeterminados de los tipos, vea Valores predeterminados de los tipos
de C#. Por tanto, dado que el constructor sin parámetros de la clase Coords inicializa todos los miembros de
datos en cero, se puede quitar por completo sin cambiar el funcionamiento de la clase. Más adelante en este
tema se proporciona un ejemplo completo del uso de varios constructores en el Ejemplo 1 y en el Ejemplo 2 se
proporciona un ejemplo de un constructor generado automáticamente.
Los constructores de instancias también se pueden usar para llamar a los constructores de instancias de las
clases base. El constructor de clase puede invocar el constructor de la clase base a través del inicializador, como
sigue:
En este ejemplo, la clase Circle pasa valores que representan el radio y el alto al constructor proporcionado
por Shape desde el que se deriva Circle . Un ejemplo completo del uso de Shape y Circle aparece en este
tema en el Ejemplo 3.
Ejemplo 1
En el ejemplo siguiente se muestra una clase con dos constructores de clase, uno sin parámetros y otro con dos
parámetros.
class Coords
{
public int x, y;
// Parameterless constructor.
public Coords()
{
x = 0;
y = 0;
}
class MainClass
{
static void Main()
{
var p1 = new Coords();
var p2 = new Coords(5, 3);
Console.WriteLine($"Coords #1 at {p1}");
Console.WriteLine($"Coords #2 at {p2}");
Console.ReadKey();
}
}
/* Output:
Coords #1 at (0,0)
Coords #2 at (5,3)
*/
Ejemplo 2
En este ejemplo, la clase Person no tiene ningún constructor, en cuyo caso, se proporciona automáticamente un
constructor sin parámetros y los campos se inicializan en sus valores predeterminados.
using System;
class TestPerson
{
static void Main()
{
var person = new Person();
Ejemplo 3
En el ejemplo siguiente se muestra cómo usar el inicializador de la clase base. La clase Circle se deriva de la
clase general Shape y la clase Cylinder se deriva de la clase Circle . En cada clase derivada, el constructor usa
su inicializador de clase base.
using System;
class TestShapes
{
static void Main()
{
double radius = 2.5;
double height = 3.0;
Vea también
Guía de programación de C#
Clases, estructuras y registros
Constructores
Finalizadores
static
Constructores privados (Guía de programación de
C#)
16/09/2021 • 2 minutes to read
Un constructor privado es un constructor de instancia especial. Se usa generalmente en clases que contienen
solo miembros estáticos. Si una clase tiene uno o más constructores privados y ningún constructor público, el
resto de clases (excepto las anidadas) no podrán crear instancias de esta clase. Por ejemplo:
class NLog
{
// Private Constructor:
private NLog() { }
Ejemplo
El siguiente es un ejemplo de clase que usa un constructor privado.
public class Counter
{
private Counter() { }
class TestCounter
{
static void Main()
{
// If you uncomment the following statement, it will generate
// an error because the constructor is inaccessible:
// Counter aCounter = new Counter(); // Error
Counter.currentCount = 100;
Counter.IncrementCount();
Console.WriteLine("New count: {0}", Counter.currentCount);
Observe que si quita el comentario de la siguiente instrucción del ejemplo, se producirá un error porque el
constructor es inaccesible debido a su nivel de protección:
Vea también
Guía de programación de C#
Clases, estructuras y registros
Constructores
Finalizadores
private
public
Constructores estáticos (Guía de programación de
C#)
16/09/2021 • 5 minutes to read
Un constructor estático se usa para inicializar cualquier dato estático o realizar una acción determinada que solo
debe realizarse una vez. Es llamado automáticamente antes de crear la primera instancia o de hacer referencia a
cualquier miembro estático.
class SimpleClass
{
// Static variable that must be initialized at run time.
static readonly long baseline;
Comentarios
Los constructores estáticos tienen las propiedades siguientes:
Un constructor estático no permite modificadores de acceso ni tiene parámetros.
Una clase o struct solo puede tener un constructor estático.
Los constructores estáticos no se pueden heredar ni sobrecargar.
No se puede llamar a un constructor estático directamente y solo está pensado para que Common
Language Runtime (CLR) lo llame. Se invoca automáticamente.
El usuario no puede controlar cuándo se ejecuta el constructor estático en el programa.
A un constructor estático se le llama automáticamente. Inicializa la clase antes de crear la primera
instancia o de hacer referencia a cualquier miembro estático. Un constructor estático se ejecuta antes que
un constructor de instancia. Se llama al constructor estático de un tipo cuando se invoca un método
estático asignado a un evento o un delegado y no cuando este se asigna. Si los inicializadores de variable
del campo estático están presentes en la clase o el constructor estático, se ejecutarán en el orden textual
en el que aparecen en la declaración. Los inicializadores se ejecutan inmediatamente antes de la ejecución
del constructor estático.
Si no proporciona un constructor estático para inicializar los campos estáticos, todos los campos estáticos
se inicializan en su valor predeterminado como se muestra en Valores predeterminados de los tipos de
C#.
Si un constructor estático inicia una excepción, el motor en tiempo de ejecución no lo invoca una segunda
vez y el tipo seguirá sin inicializar durante el período de duración del dominio de aplicación.
Normalmente, se inicia una excepción TypeInitializationException cuando un constructor estático no
puede crear una instancia de un tipo o para una excepción no controlada que se produce dentro de un
constructor estático. En el caso de los constructores estáticos no definidos de forma explícita en el código
fuente, la solución de problemas puede requerir la inspección del código de lenguaje intermedio (IL).
La presencia de un constructor estático evita la adición del atributo de tipo BeforeFieldInit. Esto limita la
optimización en tiempo de ejecución.
Un campo declarado como static readonly solo se puede asignar como parte de su declaración o en un
constructor estático. Si no se necesita un constructor estático explícito, inicialice campos estáticos en la
declaración, en lugar de a través de un constructor estático para una mejor optimización en tiempo de
ejecución.
El entorno de ejecución llama a un constructor estático no más de una vez en un dominio de aplicación
única. Esa llamada se realiza en una región bloqueada en función del tipo específico de la clase. No se
necesitan mecanismos de bloqueo adicionales en el cuerpo de un constructor estático. Para evitar el
riesgo de interbloqueos, no bloquee el subproceso actual en constructores estáticos e inicializadores. Por
ejemplo, no espere por tareas, subprocesos, identificadores de espera o eventos, no adquiera bloqueos y
no ejecute operaciones en paralelo de bloqueo, como bucles paralelos, Parallel.Invoke y consultas de
Parallel LINQ.
NOTE
Aunque no es directamente accesible, la presencia de un constructor estático explícito debe documentarse para ayudar
con la solución de problemas de excepciones de inicialización.
Uso
Los constructores estáticos se usan normalmente cuando la clase hace uso de un archivo de registro y el
constructor escribe entradas en dicho archivo.
Los constructores estáticos también son útiles al crear clases contenedoras para código no administrado,
cuando el constructor puede llamar al método LoadLibrary .
Los constructores estáticos también son un lugar adecuado para aplicar comprobaciones en tiempo de
ejecución en el parámetro de tipo que no se puede comprobar en tiempo de compilación a través de
restricciones de parámetro de tipo.
Ejemplo
En este ejemplo, la clase Bus tiene un constructor estático. Cuando se crea la primera instancia de Bus ( bus1 ),
se invoca el constructor estático para inicializar la clase. En el resultado del ejemplo, se comprueba que el
constructor estático se ejecuta solo una vez, incluso si se crean dos instancias de Bus , y que se ejecuta antes de
que se ejecute el constructor de instancia.
// Instance constructor.
public Bus(int routeNum)
{
RouteNumber = routeNum;
Console.WriteLine("Bus #{0} is created.", RouteNumber);
}
// Instance method.
public void Drive()
{
TimeSpan elapsedTime = DateTime.Now - globalStartTime;
class TestBus
{
static void Main()
{
// The creation of this instance activates the static constructor.
Bus bus1 = new Bus(71);
Vea también
Guía de programación de C#
Clases, estructuras y registros
Constructores
Clases estáticas y sus miembros
Finalizadores
Instrucciones de diseño de constructores
Advertencia de seguridad - CA2121: Los constructores estáticos deben ser privados
Procedimiento para escribir un constructor de copia
(Guía de programación de C#)
16/09/2021 • 2 minutes to read
Los registros de C# proporcionan un constructor de copia para objetos, pero debe escribir uno para las clases
personalmente.
Ejemplo
En el ejemplo siguiente, Person class define un constructor de copias que toma, como argumento, una instancia
de Person . Los valores de las propiedades de los argumentos se asignan a las propiedades de la nueva
instancia de Person . El código contiene un constructor de copias alternativo que envía las propiedades Name y
Age de la instancia que quiere copiar al constructor de instancia de la clase.
using System;
class Person
{
// Copy constructor.
public Person(Person previousPerson)
{
Name = previousPerson.Name;
Age = previousPerson.Age;
}
// Instance constructor.
public Person(string name, int age)
{
Name = name;
Age = age;
}
class TestPerson
{
static void Main()
{
// Create a Person object by using the instance constructor.
Person person1 = new Person("George", 40);
// Show details to verify that the name and age fields are distinct.
Console.WriteLine(person1.Details());
Console.WriteLine(person2.Details());
Vea también
ICloneable
Registros
Guía de programación de C#
Clases, estructuras y registros
Constructores
Finalizadores
Inicializadores de objeto y de colección (Guía de
programación de C#)
16/09/2021 • 10 minutes to read
C# permite crear instancias de un objeto o colección y realizar asignaciones de miembros en una sola
instrucción.
Inicializadores de objeto
Los inicializadores de objeto permiten asignar valores a cualquier campo o propiedad accesible de un objeto en
el momento de su creación sin tener que invocar un constructor seguido de líneas de instrucciones de
asignación. La sintaxis de inicializador de objetos permite especificar argumentos para un constructor u omitir
los argumentos (y la sintaxis de paréntesis). En el ejemplo siguiente se muestra cómo usar un inicializador de
objeto con un tipo con nombre, Cat , y cómo invocar el constructor sin parámetros. Tenga en cuenta el uso de
propiedades implementadas automáticamente en la clase Cat . Para obtener más información, vea Propiedades
implementadas automáticamente.
public Cat()
{
}
La sintaxis de los inicializadores de objeto le permite crear una instancia, y después asigna el objeto recién
creado, con las propiedades asignadas, a la variable de la asignación.
A partir de C# 6, los inicializadores de objeto pueden establecer indizadores, además de asignar campos y
propiedades. Tenga en cuenta esta clase básica Matrix :
public class Matrix
{
private double[,] storage = new double[3, 3];
[1, 0] = 0.0,
[1, 1] = 1.0,
[1, 2] = 0.0,
[2, 0] = 0.0,
[2, 1] = 0.0,
[2, 2] = 1.0,
};
Puede usarse cualquier indizador accesible que contenga un establecedor accesible como una de las
expresiones de un inicializador de objeto, independientemente del número o los tipos de argumentos. Los
argumentos del índice forman el lado izquierdo de la asignación, mientras que el valor es el lado derecho de la
expresión. Por ejemplo, todos estos son válidos si IndexersExample tiene los indizadores adecuados:
Para que el código anterior se compile, el tipo IndexersExample debe tener los siguientes miembros:
Los tipos anónimos permiten a la cláusula select de una expresión de consulta LINQ transformar objetos de la
secuencia original en objetos cuyo valor y forma pueden ser distintos de los originales. Esto resulta útil si desea
almacenar solo una parte de la información de cada objeto en una secuencia. En el ejemplo siguiente, suponga
que un objeto del producto ( p ) contiene numerosos campos y métodos y que solo le interesa crear una
secuencia de objetos que contenga el nombre del producto y el precio por unidad.
var productInfos =
from p in products
select new { p.ProductName, p.UnitPrice };
Al ejecutarse esta consulta, la variable productInfos incluirá una secuencia de objetos a la que se puede tener
acceso en una instrucción foreach , como se muestra en este ejemplo:
foreach(var p in productInfos){...}
Cada objeto del nuevo tipo anónimo tiene dos propiedades públicas que reciben los mismos nombres que las
propiedades o los campos del objeto original. También puede cambiar el nombre de un campo al crear un tipo
anónimo; en el ejemplo siguiente se cambia el nombre del campo UnitPrice a Price .
Inicializadores de colección
Los inicializadores de colección le permiten especificar uno o varios inicializadores de elemento al inicializar un
tipo de colección que implementa IEnumerable y tiene Add con la firma apropiada como un método de
instancia o un método de extensión. Los inicializadores de elemento pueden ser un valor simple, una expresión
o un inicializador de objeto. Si se usa un inicializador de colección, no es necesario especificar varias llamadas; el
compilador las agrega automáticamente.
En el ejemplo siguiente se muestran dos inicializadores de colección simples:
El inicializador de colección siguiente usa inicializadores de objeto para inicializar los objetos de la clase Cat
definida en un ejemplo anterior. Observe que los inicializadores de objeto individuales se escriben entre llaves y
se separan por comas.
Puede especificar null como elemento de un inicializador de colección si el método Add de la colección lo
permite.
List<Cat> moreCats = new List<Cat>
{
new Cat{ Name = "Furrytail", Age=5 },
new Cat{ Name = "Peaches", Age=4 },
null
};
El ejemplo anterior genera código que llama a Item[TKey] para establecer los valores. A partir de C# 6, puede
inicializar diccionarios y otros contenedores asociativos con la sintaxis siguiente. Tenga en cuenta que en lugar
de sintaxis de indizador, con paréntesis y una asignación, usa un objeto con varios valores:
Este ejemplo de inicializador llama a Add(TKey, TValue) para agregar los tres elementos al diccionario. Estas dos
maneras distintas de inicializar colecciones asociativas tienen un comportamiento ligeramente diferente debido
a las llamadas a métodos que genera el compilador. Ambas variantes funcionan con la clase Dictionary . Es
posible que otros tipos solo admitan una o la otra, en función de su API pública.
No podrá usar la sintaxis del inicializador de colección abordada hasta ahora, ya que no se puede asignar una
nueva lista a la propiedad:
El conjunto de entradas que se van a agregar simplemente aparecen entre llaves. Lo anterior es idéntico a
escribir lo siguiente:
Ejemplos
En el ejemplo siguiente se combinan los conceptos de inicializadores de objeto y colección.
public class InitializationSample
{
public class Cat
{
// Auto-implemented properties.
public int Age { get; set; }
public string Name { get; set; }
public Cat() { }
// Display results.
System.Console.WriteLine(cat.Name);
El ejemplo siguiente muestra un objeto que implementa IEnumerable y que contiene un método Add con varios
parámetros. Usa un inicializador de colección con varios elementos por cada elemento de la lista
correspondiente a la firma del método Add .
public class FullExample
{
class FormattedAddresses : IEnumerable<string>
{
private List<string> internalList = new List<string>();
public IEnumerator<string> GetEnumerator() => internalList.GetEnumerator();
Console.WriteLine("Address Entries:");
/*
* Prints:
Address Entries:
John Doe
123 Street
Topeka, KS 00000
Jane Smith
456 Street
Topeka, KS 00000
*/
}
Los métodos Add pueden usar la palabra clave params para tomar un número variable de argumentos, como
se muestra en el ejemplo siguiente. En este ejemplo además se muestra la implementación personalizada de un
indizador para inicializar una colección mediante índices.
public void Add(TKey key, params TValue[] values) => Add(key, (IEnumerable<TValue>)values);
storedValues.AddRange(values);
}
}
/*
* Prints:
Using second multi-valued dictionary created with a collection initializer using indexing:
Using third multi-valued dictionary created with a collection initializer using indexing:
Vea también
Guía de programación de C#
LINQ en C#
Tipos anónimos
Procedimiento para inicializar objetos usando un
inicializador de objeto (Guía de programación de
C#)
16/09/2021 • 3 minutes to read
Puede usar inicializadores de objeto para inicializar objetos de tipo de una forma declarativa sin tener que
invocar explícitamente un constructor para el tipo.
En los siguientes ejemplos se muestra cómo usar los inicializadores de objeto con objetos con nombre. El
compilador procesa los inicializadores de objeto primero obteniendo acceso al constructor de instancia sin
parámetros y después procesando las inicializaciones de miembro. Por lo tanto, si el constructor sin parámetros
se declara como private en la clase, se producirá un error en los inicializadores de objeto que requieren acceso
público.
Debe usar un inicializador de objeto si va a definir un tipo anónimo. Para obtener más información, vea
Procedimiento para devolver subconjuntos de propiedades de elementos en una consulta.
Ejemplo
En el siguiente ejemplo se muestra cómo inicializar un nuevo tipo StudentName usando inicializadores de objeto.
Este ejemplo establece propiedades en el tipo StudentName :
Console.WriteLine(student1.ToString());
Console.WriteLine(student2.ToString());
Console.WriteLine(student3.ToString());
Console.WriteLine(student4.ToString());
}
// Output:
// Craig 0
// Craig 0
// 183
// Craig 116
// Properties.
public string FirstName { get; set; }
public string LastName { get; set; }
public int ID { get; set; }
Los inicializadores de objeto pueden usarse para establecer indizadores en un objeto. En el ejemplo siguiente se
define una clase BaseballTeam que usa un indizador para obtener y establecer jugadores en posiciones
diferentes. El inicializador puede asignar jugadores en función de la abreviatura de la posición o del número
usado para las puntuaciones de béisbol de cada posición:
public class HowToIndexInitializer
{
public class BaseballTeam
{
private string[] players = new string[9];
private readonly List<string> positionAbbreviations = new List<string>
{
"P", "C", "1B", "2B", "3B", "SS", "LF", "CF", "RF"
};
Console.WriteLine(team["2B"]);
}
}
Vea también
Guía de programación de C#
Inicializadores de objeto y colección
Procedimientos: inicialización de un diccionario con
un inicializador de colección (guía de programación
de C#)
16/09/2021 • 2 minutes to read
Una clase Dictionary<TKey,TValue> contiene una colección de pares clave-valor. Su método Add toma dos
parámetros: uno para la clave y otro para el valor. Una manera de inicializar Dictionary<TKey,TValue>, o
cualquier colección cuyo método Add tome varios parámetros, es incluir entre llaves cada conjunto de
parámetros, como se muestra en el ejemplo siguiente. Otra opción es usar un inicializador de índice, lo que
también se muestra en el ejemplo siguiente.
Ejemplo
En el ejemplo de código siguiente, Dictionary<TKey,TValue> se inicializa con instancias de tipo StudentName . La
primera inicialización usa el método Add con dos argumentos. El compilador genera una llamada a Add por
cada uno de los pares de claves int y valores StudentName . La segunda usa un método de indizador de lectura
y escritura público de la clase Dictionary :
public class HowToDictionaryInitializer
{
class StudentName
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int ID { get; set; }
}
Observe los dos pares de llaves de cada elemento de la colección en la primera declaración. Las llaves internas
contienen el inicializador de objeto para StudentName , mientras que las externas contienen el inicializador para
el par clave-valor que se va a agregar a la clase Dictionary<TKey,TValue> students . Por último, el inicializador
completo de la colección para el diccionario se encierra entre llaves. En la segunda inicialización, el lado
izquierdo de la asignación es la clave y el lado derecho es el valor, con un inicializador de objeto para
StudentName .
Vea también
Guía de programación de C#
Inicializadores de objeto y colección
Tipos anidados (Guía de programación de C#)
16/09/2021 • 2 minutes to read
Un tipo definido en una clase, estructura o interfaz se denomina tipo anidado. Por ejemplo
Con independencia de si el tipo externo es una clase, una interfaz o una estructura, los tipos anidados se
establecen de manera predeterminada en private; solo son accesibles desde su tipo contenedor. En el ejemplo
anterior, la clase Nested es inaccesible a los tipos externos.
También puede especificar un modificador de acceso para definir la accesibilidad de un tipo anidado, de la
manera siguiente:
Los tipos anidados de una clase pueden ser public, protected, internal, protected internal, private o
private protected.
En cambio, al definir una clase anidada protected , protected internal o private protected dentro de
una clase sellada, se genera una advertencia del compilador CS0628, "Nuevo miembro protegido
declarado en la clase sealed".
Tenga en cuenta también que la creación de un tipo anidado externamente visible infringe la regla de
calidad del código CA1034 "Los tipos anidados no deben ser visibles".
Los tipos anidados de un struct pueden ser public, internal o private.
En el ejemplo siguiente se convierte la clase Nested en public:
El tipo anidado o interno puede tener acceso al tipo contenedor o externo. Para tener acceso al tipo contenedor,
páselo como un argumento al constructor del tipo anidado. Por ejemplo:
public class Container
{
public class Nested
{
private Container parent;
public Nested()
{
}
public Nested(Container parent)
{
this.parent = parent;
}
}
}
Un tipo anidado tiene acceso a todos los miembros que estén accesibles para el tipo contenedor. Puede tener
acceso a los miembros privados y protegidos del tipo contenedor, incluidos los miembros protegidos
heredados.
En la declaración anterior, el nombre completo de la clase Nested es Container.Nested . Este es el nombre que
se utiliza para crear una instancia nueva de la clase anidada, de la siguiente manera:
Vea también
Guía de programación de C#
Clases, estructuras y registros
Modificadores de acceso
Constructores
Regla CA1034
Clases y métodos parciales (Guía de programación
de C#)
16/09/2021 • 7 minutes to read
Es posible dividir la definición de una clase, un struct, una interfaz o un método en dos o más archivos de código
fuente. Cada archivo de código fuente contiene una sección de la definición de tipo o método, y todos los
elementos se combinan cuando se compila la aplicación.
Clases parciales
Es recomendable dividir una definición de clase en varias situaciones:
Cuando se trabaja con proyectos grandes, el hecho de repartir una clase entre archivos independientes
permite que varios programadores trabajen en ella al mismo tiempo.
Cuando se trabaja con código fuente generado automáticamente, se puede agregar código a la clase sin
tener que volver a crear el archivo de código fuente. Visual Studio usa este enfoque al crear formularios
Windows Forms, código de contenedor de servicio Web, etc. Puede crear código que use estas clases sin
necesidad de modificar el archivo creado por Visual Studio.
Al usar generadores de código fuente para generar funcionalidades adicionales en una clase.
Para dividir una definición de clase, use el modificador de palabra clave partial, como se muestra aquí:
La palabra clave partial indica que se pueden definir en el espacio de nombres otros elementos de la clase, la
estructura o la interfaz. Todos los elementos deben usar la palabra clave partial . Todos los elementos deben
estar disponibles en tiempo de compilación para formar el tipo final. Todos los elementos deben tener la misma
accesibilidad, como public , private , etc.
Si algún elemento se declara abstracto, todo el tipo se considera abstracto. Si algún elemento se declara sellado,
todo el tipo se considera sellado. Si algún elemento declara un tipo base, todo el tipo hereda esa clase.
Todos los elementos que especifiquen una clase base deben coincidir, pero los elementos que omitan una clase
base heredan igualmente el tipo base. Los elementos pueden especificar diferentes interfaces base, y el tipo final
implementa todas las interfaces enumeradas por todas las declaraciones parciales. Todas las clases, structs o
miembros de interfaz declarados en una definición parcial están disponibles para todos los demás elementos. El
tipo final es la combinación de todos los elementos en tiempo de compilación.
NOTE
El modificador partial no está disponible en declaraciones de delegado o enumeración.
En el ejemplo siguiente se muestra que los tipos anidados pueden ser parciales, incluso si el tipo en el que están
anidados no es parcial.
class Container
{
partial class Nested
{
void Test() { }
}
En tiempo de compilación, se combinan los atributos de definiciones de tipo parcial. Por ejemplo, consideremos
las siguientes declaraciones:
[SerializableAttribute]
partial class Moon { }
[ObsoleteAttribute]
partial class Moon { }
[SerializableAttribute]
[ObsoleteAttribute]
class Moon { }
A continuación se indican los elementos que se combinan de todas las definiciones de tipo parcial:
comentarios XML
interfaces
atributos de parámetro de tipo genérico
class (atributos)
miembros
Por ejemplo, consideremos las siguientes declaraciones:
Restricciones
Debe seguir varias reglas al trabajar con definiciones de clase parcial:
Todas las definiciones de tipo parcial que van a formar parte del mismo tipo deben modificarse con partial .
Por ejemplo, las declaraciones de clase siguientes generan un error: [!code-
csharpAllDefinitionsMustBePartials#7].
El modificador partial solo puede aparecer inmediatamente antes de las palabras clave class , struct o
interface .
Se permiten tipos parciales anidados en definiciones de tipo parcial, como se muestra en el ejemplo
siguiente: [!code-csharpNestedPartialTypes#8].
Todas las definiciones de tipo parcial que van a formar parte del mismo tipo deben definirse en el mismo
ensamblado y en el mismo módulo (archivo .exe o .dll). Las definiciones parciales no pueden abarcar varios
módulos.
El nombre de clase y los parámetros de tipo genérico deben coincidir en todas las definiciones de tipo
parcial. Los tipos genéricos pueden ser parciales. Cada declaración parcial debe usar los mismos nombres de
parámetro en el mismo orden.
Las siguientes palabras clave son opcionales en una definición de tipo parcial, pero si están presentes la
definición, no pueden entrar en conflicto con las palabras clave especificadas en otra definición parcial para
el mismo tipo:
public
private
protected
internal
abstract
sealed
clase base
modificador new (elementos anidados)
restricciones genéricas
Para obtener más información, vea Restricciones de tipos de parámetros.
Ejemplos
En el ejemplo siguiente, los campos y el constructor de la clase, Coords , se declaran en una definición de clase
parcial y el miembro PrintCoords se declara en otra definición de clase parcial.
public partial class Coords
{
private int x;
private int y;
class TestCoords
{
static void Main()
{
Coords myCoords = new Coords(10, 15);
myCoords.PrintCoords();
En el ejemplo siguiente se muestra que también se pueden desarrollar structs e interfaces parciales.
partial struct S1
{
void Struct_Test() { }
}
partial struct S1
{
void Struct_Test2() { }
}
Métodos Partial
Una clase o struct parcial puede contener un método parcial. Un elemento de la clase contiene la firma del
método. Una implementación se puede definir en el mismo elemento o en otro. Si no se proporciona la
implementación, el método y todas las llamadas al método se quitan en tiempo de compilación. La
implementación puede ser necesaria en función de la signatura del método. No es necesario que un método
parcial tenga una implementación en los casos siguientes:
No tiene ningún modificador de accesibilidad (incluido el predeterminado private).
Devuelve void.
No tiene parámetros out.
No tiene ninguno de los modificadores virtual, override, sealed, new o extern.
Cualquier método que no cumpla todas estas restricciones (por ejemplo, public virtual partial void ) debe
proporcionar una implementación. Esa implementación la puede proporcionar un generador de código fuente.
Los métodos parciales permiten que el implementador de una parte de una clase declare un método. El
implementador de otra parte de la clase puede definir ese método. Hay dos escenarios en los que esto es útil:
plantillas que generan código reutilizable y generadores de código fuente.
Código de plantilla : la plantilla reserva un nombre de método y una firma para que el código generado
pueda llamar al método. Estos métodos siguen las restricciones que permiten a un desarrollador decidir si
implementar el método. Si el método no se implementa, el compilador quita la firma del método y todas las
llamadas al método. Las llamadas al método, incluidos los resultados que se producirían por la evaluación de
los argumentos de las llamadas, no tienen efecto en tiempo de ejecución. Por lo tanto, el código de la clase
parcial puede usar libremente un método parcial, incluso si no se proporciona la implementación. No se
producirá ningún error en tiempo de compilación o en tiempo de ejecución si se llama al método pero no se
implementa.
Generadores de código fuente : los generadores de código fuente proporcionan una implementación
para los métodos. El desarrollador humano puede agregar la declaración de método (a menudo con
atributos leídos por el generador de código fuente). El desarrollador puede escribir código que llame a estos
métodos. El generador de código fuente se ejecuta durante la compilación y proporciona la implementación.
En este escenario, no se suelen seguir las restricciones de los métodos parciales que pueden no
implementarse.
// Definition in file1.cs
partial void OnNameChanged();
// Implementation in file2.cs
partial void OnNameChanged()
{
// method body
}
Las declaraciones de método parcial deben comenzar con la palabra clave contextual partial.
Las signaturas de métodos parciales de los dos elementos del tipo parcial deben coincidir.
Los métodos parciales pueden tener modificadores static y unsafe.
Los métodos parciales pueden ser genéricos. Las restricciones se colocan en la declaración de método parcial
de definición y opcionalmente pueden repetirse en el de implementación. Los nombres del parámetro y del
parámetro de tipo no tienen que ser iguales en la declaración de implementación y en la declaración de
definición.
Puede crear un delegado para un método parcial que se ha definido e implementado, pero no para un
método parcial que solo se ha definido.
Vea también
Guía de programación de C#
Clases
Tipos de estructura
Interfaces
partial (Tipos)
Procedimiento para devolver subconjuntos de
propiedades de elementos en una consulta (Guía de
programación de C#)
16/09/2021 • 2 minutes to read
Use un tipo anónimo en una expresión de consulta cuando se cumplan estas dos condiciones:
Solo quiere algunas de las propiedades de cada elemento de origen.
No es necesario almacenar los resultados de la consulta fuera del ámbito del método en el que se ejecuta
la consulta.
Si solo quiere devolver una propiedad o campo de cada elemento de origen, puede usar simplemente el
operador de punto en la cláusula select . Por ejemplo, para devolver solo el ID de cada student , escriba la
cláusula select como sigue:
select student.ID;
Ejemplo
En el ejemplo siguiente se muestra cómo usar un tipo anónimo para devolver solo un subconjunto de las
propiedades de cada elemento de origen que coincida con la condición especificada.
Tenga en cuenta que, si no se especifica ningún nombre, el tipo anónimo usa los nombres del elemento de
origen para sus propiedades. Para asignar nombres nuevos a las propiedades del tipo anónimo, escriba la
instrucción select como sigue:
select new { First = student.FirstName, Last = student.LastName };
Compilar el código
Para ejecutar este código, copie y pegue la clase en una aplicación de consola de C# con una directiva using de
System.Linq.
Vea también
Guía de programación de C#
Tipos anónimos
LINQ en C#
Implementación de interfaz explícita (Guía de
programación de C#)
16/09/2021 • 3 minutes to read
Si una clase implementa dos interfaces que contienen un miembro con la misma firma, entonces al implementar
ese miembro en la clase ambas interfaces usarán ese miembro como su implementación. En el ejemplo
siguiente, todas las llamadas a Paint invocan el mismo método. En este primer ejemplo se definen los tipos:
// Output:
// Paint method in SampleClass
// Paint method in SampleClass
// Paint method in SampleClass
Pero es posible que no quiera que se llame a la misma implementación para las dos interfaces. Para llamar a
otra implementación en función de la interfaz en uso, puede implementar un miembro de interfaz de forma
explícita. Una implementación de interfaz explícita es un miembro de clase al que solo se llama a través de la
interfaz especificada. Asigne al miembro de clase el nombre de la interfaz y un punto como prefijo. Por ejemplo:
public class SampleClass : IControl, ISurface
{
void IControl.Paint()
{
System.Console.WriteLine("IControl.Paint");
}
void ISurface.Paint()
{
System.Console.WriteLine("ISurface.Paint");
}
}
El miembro de clase IControl.Paint solo está disponible a través de la interfaz IControl , y ISurface.Paint
solo está disponible mediante ISurface . Las dos implementaciones de método son independientes, y ninguna
está disponible directamente en la clase. Por ejemplo:
// Output:
// IControl.Paint
// ISurface.Paint
La implementación explícita también se usa para resolver casos donde dos interfaces declaran miembros
diferentes del mismo nombre como una propiedad y un método. Para implementar ambas interfaces, una clase
tiene que usar la implementación explícita para la propiedad P o el método P , o ambos, para evitar un error
del compilador. Por ejemplo:
interface ILeft
{
int P { get;}
}
interface IRight
{
int P();
}
Una implementación de interfaz explícita no tiene un modificador de acceso, ya que no es accesible como
miembro del tipo en el que se define. En su lugar, solo es accesible cuando se llama mediante una instancia de la
interfaz. Si especifica un modificador de acceso para una implementación de interfaz explícita, obtendrá el error
del compilador CS0106. Para obtener más información, vea interface (Referencia de C#).
A partir de C# 8.0, se puede definir una implementación para los miembros declarados en una interfaz. Si una
clase hereda una implementación de método de una interfaz, ese método solo es accesible a través de una
referencia del tipo de interfaz. El miembro heredado no aparece como parte de la interfaz pública. En el ejemplo
siguiente se define una implementación predeterminada para un método de interfaz:
public interface IControl
{
void Paint() => Console.WriteLine("Default Paint method");
}
public class SampleClass : IControl
{
// Paint() is inherited from IControl.
}
Cualquier clase que implemente la interfaz IControl puede invalidar el método Paint predeterminado, ya sea
como un método público, o bien como una implementación de interfaz explícita.
Consulte también
Guía de programación de C#
Clases, estructuras y registros
Interfaces
Herencia
Procedimiento Implementar explícitamente
miembros de interfaz (Guía de programación de
C#)
16/09/2021 • 2 minutes to read
Este ejemplo declara una interfaz, IDimensions , y una clase, Box , que implementa explícitamente los miembros
de interfaz GetLength y GetWidth . Se tiene acceso a los miembros mediante la instancia de interfaz dimensions .
Ejemplo
interface IDimensions
{
float GetLength();
float GetWidth();
}
Programación sólida
Tenga en cuenta que las siguientes líneas, en el método Main , se comentan porque producirían errores
de compilación. No se puede tener acceso a un miembro de interfaz que se implementa explícitamente
desde una instancia class:
Tenga en cuenta también que las líneas siguientes, en el método Main , imprimen correctamente las
dimensiones del cuadro porque se llama a los métodos desde una instancia de la interfaz:
System.Console.WriteLine("Length: {0}", dimensions.GetLength());
System.Console.WriteLine("Width: {0}", dimensions.GetWidth());
Consulte también
Guía de programación de C#
Clases, estructuras y registros
Interfaces
Procedimiento para implementar miembros de dos interfaces de forma explícita
Procedimiento Implementar explícitamente
miembros de dos interfaces (Guía de programación
de C#)
16/09/2021 • 2 minutes to read
La implementación explícita de interfaces también permite al programador implementar dos interfaces que
tienen los mismos nombres de miembros y dar a cada miembro de una interfaz una implementación
independiente. En este ejemplo se muestran las dimensiones de un cuadro en unidades métricas e inglesas. La
clase Box implementa dos interfaces, IEnglishDimensions e IMetricDimensions, que representan los diferentes
sistemas de medida. Ambas interfaces tienen nombres de miembros idénticos, Length y Width.
Ejemplo
// Declare the English units interface:
interface IEnglishDimensions
{
float Length();
float Width();
}
Programación sólida
Si quiere realizar las medidas en unidades inglesas de manera predeterminada, implemente los métodos Length
y Width con normalidad e implemente explícitamente los métodos Length y Width de la interfaz
IMetricDimensions:
// Normal implementation:
public float Length() => lengthInches;
public float Width() => widthInches;
// Explicit implementation:
float IMetricDimensions.Length() => lengthInches * 2.54f;
float IMetricDimensions.Width() => widthInches * 2.54f;
En este caso, se puede tener acceso a las unidades inglesas desde la instancia de clase y acceso a las unidades
métricas desde la instancia de interfaz:
Consulte también
Guía de programación de C#
Clases, estructuras y registros
Interfaces
Procedimiento para implementar miembros de interfaz de forma explícita
Delegados (Guía de programación de C#)
16/09/2021 • 3 minutes to read
Un delegado es un tipo que representa referencias a métodos con una lista de parámetros determinada y un
tipo de valor devuelto. Cuando se crea una instancia de un delegado, puede asociar su instancia a cualquier
método mediante una signatura compatible y un tipo de valor devuelto. Puede invocar (o llamar) al método a
través de la instancia del delegado.
Los delegados se utilizan para pasar métodos como argumentos a otros métodos. Los controladores de eventos
no son más que métodos que se invocan a través de delegados. Cree un método personalizado y una clase,
como un control de Windows, podrá llamar al método cuando se produzca un determinado evento. En el
siguiente ejemplo se muestra una declaración de delegado:
Cualquier método de cualquier clase o struct accesible que coincida con el tipo de delegado se puede asignar al
delegado. El método puede ser estático o de instancia. Esta flexibilidad significa que puede cambiar las llamadas
de método mediante programación, o bien agregar código nuevo a las clases existentes.
NOTE
En el contexto de la sobrecarga de métodos, la signatura de un método no incluye el valor devuelto. Sin embargo, en el
contexto de los delegados, la signatura sí lo incluye. En otras palabras, un método debe tener el mismo tipo de valor
devuelto que el delegado.
Esta capacidad de hacer referencia a un método como parámetro hace que los delegados sean idóneos para
definir métodos de devolución de llamada. Puede escribir un método que compare dos objetos en la aplicación.
Ese método se puede usar en un delegado para un algoritmo de ordenación. Como el código de comparación es
independiente de la biblioteca, el método de ordenación puede ser más general.
Se han agregado punteros de función a C# 9 para escenarios similares, donde se necesita más control sobre la
convención de llamadas. El código asociado a un delegado se invoca mediante un método virtual agregado a un
tipo de delegado. Mediante los punteros de función puede especificar otras convenciones.
En esta sección
Utilizar delegados
Cuándo usar delegados en lugar de interfaces (Guía de programación de C#)
Delegados con métodos con nombre y delegados con métodos anónimos
Uso de varianza en delegados
Procedimiento para combinar delegados (delegados de multidifusión)
Procedimiento para declarar un delegado, crear instancias del mismo y usarlo
Vea también
Delegate
Guía de programación de C#
Eventos
Utilizar delegados (Guía de programación de C#)
16/09/2021 • 5 minutes to read
Un delegado es un tipo que encapsula de forma segura un método, similar a un puntero de función en C y C++.
A diferencia de los punteros de función de C, los delegados están orientados a objetos, proporcionan seguridad
de tipos y son seguros. El tipo de un delegado se define por el nombre del delegado. En el ejemplo siguiente, se
declara un delegado denominado Del que puede encapsular un método que toma una string como argumento
y devuelve void:
Normalmente, un objeto delegado se construye al proporcionar el nombre del método que el delegado
encapsulará o con una función anónima. Una vez que se crea una instancia de delegado, el delegado pasará al
método una llamada de método realizada al delegado. Los parámetros pasados al delegado por el autor de la
llamada se pasan a su vez al método, y el valor devuelto desde el método, si lo hubiera, es devuelto por el
delegado al autor de la llamada. Esto se conoce como invocar al delegado. Un delegado con instancias se puede
invocar como si fuera el propio método encapsulado. Por ejemplo:
Los tipos de delegado se derivan de la clase Delegate en .NET. Los tipos de delegados son sealed (no se pueden
derivar) y no se pueden derivar clases personalizadas de Delegate. Dado que el delegado con instancias es un
objeto, puede pasarse como parámetro o asignarse a una propiedad. De este modo, un método puede aceptar
un delegado como parámetro y llamar al delegado en algún momento posterior. Esto se conoce como
devolución de llamada asincrónica y es un método común para notificar a un llamador que un proceso largo ha
finalizado. Cuando se utiliza un delegado de esta manera, el código que usa al delegado no necesita ningún
conocimiento de la implementación del método empleado. La funcionalidad es similar a la encapsulación que
proporcionan las interfaces.
Otro uso común de devoluciones de llamada es definir un método de comparación personalizado y pasar ese
delegado a un método de ordenación. Permite que el código del llamador se convierta en parte del algoritmo de
ordenación. En el siguiente método de ejemplo se usa el tipo Del como parámetro:
Junto con el DelegateMethod estático mostrado anteriormente, ahora tenemos tres métodos que se pueden
encapsularse mediante una instancia de Del .
Un delegado puede llamar a más de un método cuando se invoca. Esto se conoce como multidifusión. Para
agregar un método adicional a la lista de métodos del delegado —la lista de invocación—, simplemente es
necesario agregar dos delegados mediante los operadores de adición o asignación y suma ('+' o '+='). Por
ejemplo:
En este momento allMethodsDelegate contiene tres métodos en su lista de invocación: Method1 , Method2 y
DelegateMethod . Los tres delegados originales, d1 , d2 y d3 , permanecen sin cambios. Cuando se invoca
allMethodsDelegate , todos los métodos se llaman en orden. Si el delegado usa parámetros de referencia, la
referencia se pasa secuencialmente a cada uno de los tres métodos por turnos, y cualquier cambio que realice
un método es visible para el siguiente método. Cuando alguno de los métodos produce una excepción que no
se captura dentro del método, esa excepción se pasa al llamador del delegado y no se llama a los métodos
siguientes de la lista de invocación. Si el delegado tiene un valor devuelto o los parámetros de salida, devuelve
el valor devuelto y los parámetros del último método invocado. Para quitar un método de la lista de invocación,
utilice los operadores de decremento o de asignación de decremento ( - o -= ). Por ejemplo:
//remove Method1
allMethodsDelegate -= d1;
Dado que los tipos de delegado se derivan de System.Delegate , los métodos y propiedades definidas por esa
clase se pueden llamar en el delegado. Por ejemplo, para buscar el número de métodos en la lista de invocación
de un delegado, se puede escribir:
Los delegados con más de un método en su lista de invocación derivan de MulticastDelegate, que es una
subclase de System.Delegate . El código anterior funciona en ambos casos porque las dos clases admiten
GetInvocationList .
Los delegados de multidifusión se utilizan mucho en el control de eventos. Los objetos de origen de evento
envían notificaciones de evento a los objetos de destinatario que se han registrado para recibir ese evento. Para
suscribirse a un evento, el destinatario crea un método diseñado para controlar el evento; a continuación, crea a
un delegado para dicho método y pasa el delegado al origen de eventos. El origen llama al delegado cuando se
produce el evento. Después, el delegado llama al método que controla los eventos en el destinatario y entrega
los datos del evento. El origen del evento define el tipo de delegado para un evento determinado. Para obtener
más información, consulte Eventos.
La comparación de delegados de dos tipos distintos asignados en tiempo de compilación generará un error de
compilación. Si las instancias de delegado son estáticamente del tipo System.Delegate , entonces se permite la
comparación, pero devolverá false en tiempo de ejecución. Por ejemplo:
Consulte también
Guía de programación de C#
Delegados
Uso de varianza en delegados
Varianza en delegados
Uso de varianza para los delegados genéricos Func y Action
Eventos
Delegados con métodos con nombre y Métodos
anónimos (Guía de programación de C#)
16/09/2021 • 2 minutes to read
Un delegado puede asociarse con un método con nombre. Cuando crea una instancia de un delegado mediante
un método con nombre, el método se pasa como un parámetro, por ejemplo:
// Declare a delegate.
delegate void Del(int x);
Esto se llama con un método con nombre. Los delegados construidos con un método con nombre pueden
encapsular un método estático o un método de instancia. Los métodos con nombre son la única manera de
crear una instancia de un delegado en versiones anteriores de C#. En cambio, en una situación en la que crear
un método nuevo es una sobrecarga no deseada, C# le permite crear una instancia de un delegado y especificar
inmediatamente un bloque de código que el delegado procesará cuando se llame. El bloque puede contener una
expresión lambda o un método anónimo. Para obtener más información, vea Funciones anónimas.
Comentarios
El método que pasa como un parámetro de delegado debe tener la misma firma que la declaración de delegado.
Una instancia de delegado puede encapsular un método de instancia o estático.
Aunque el delegado puede usar un parámetro out, no recomendamos su uso con delegados de eventos de
multidifusión porque no puede conocer a qué delegado se llamará.
Ejemplo 1
A continuación se muestra un ejemplo sencillo de cómo declarar y usar un delegado. Tenga en cuenta que tanto
el delegado, Del , como el método asociado, MultiplyNumbers , tienen la misma firma
// Declare a delegate
delegate void Del(int i, double j);
class MathClass
{
static void Main()
{
MathClass m = new MathClass();
Ejemplo 2
En el ejemplo siguiente, un delegado se asigna a métodos estáticos y de instancia y devuelve información
específica de cada uno.
// Declare a delegate
delegate void Del();
class SampleClass
{
public void InstanceMethod()
{
Console.WriteLine("A message from the instance method.");
}
class TestSampleClass
{
static void Main()
{
var sc = new SampleClass();
Consulte también
Guía de programación de C#
Delegados
Procedimiento para combinar delegados (delegados de multidifusión)
Eventos
Procedimiento para combinar delegados
(delegados de multidifusión) (Guía de programación
de C#)
16/09/2021 • 2 minutes to read
En este ejemplo se muestra cómo crear delegados de multidifusión. Una propiedad útil de los objetos delegados
es que puedan asignarse objetos múltiples a una instancia de delegado con el operador + . El delegado de
multidifusión contiene una lista de los delegados asignados. Cuando se llama al delegado de multidifusión,
invoca a los delegados de la lista, en orden. Solo los delegados del mismo tipo pueden combinarse.
El operador - puede usarse para quitar un delegado de componente de un delegado de multidifusión.
Ejemplo
using System;
// Define a custom delegate that has a string parameter and returns void.
delegate void CustomDel(string s);
class TestClass
{
// Define two methods that have the same signature as CustomDel.
static void Hello(string s)
{
Console.WriteLine($" Hello, {s}!");
}
En C# 1.0 y versiones posteriores, los delegados se pueden declarar como se muestra en el ejemplo siguiente.
// Declare a delegate.
delegate void Del(string str);
C# 2.0 ofrece una forma más sencilla de escribir la declaración anterior, tal como se muestra en el ejemplo
siguiente.
En C# 2.0 y versiones posteriores, también es posible utilizar un método anónimo para declarar e inicializar un
delegado, como se muestra en el ejemplo siguiente.
En C# 3.0 y versiones posteriores, también se pueden declarar los delegados y crear una instancia de ellos
mediante una expresión lambda, como se muestra en el ejemplo siguiente.
Ejemplo
// A set of classes for handling a bookstore:
namespace Bookstore
{
using System.Collections;
Cada tipo de delegado describe el número y los tipos de argumentos, y el tipo del valor devuelto de los
métodos que puede encapsular. Siempre que se necesite un nuevo conjunto de tipos de argumentos o de
tipos de valores devueltos, se debe declarar un nuevo tipo de delegado.
Creación de instancias de un delegado.
Después de haber declarado un tipo de delegado, debe crearse un objeto delegado y asociarse con un
método particular. En el ejemplo anterior, esto se hace pasando el método PrintTitle al método
ProcessPaperbackBooks como en el ejemplo siguiente:
bookDB.ProcessPaperbackBooks(PrintTitle);
Esto crea un objeto delegado nuevo asociado con el método estático Test.PrintTitle . De forma similar,
el método no estático AddBookToTotal del objeto totaller se pasa como en el ejemplo siguiente:
bookDB.ProcessPaperbackBooks(totaller.AddBookToTotal);
processBook(b);
Se puede llamar a un delegado de forma sincrónica, como en este ejemplo, o bien de forma asincrónica
con los métodos BeginInvoke y EndInvoke .
Consulte también
Guía de programación de C#
Eventos
Delegados
Matrices (Guía de programación de C#)
16/09/2021 • 3 minutes to read
Puede almacenar varias variables del mismo tipo en una estructura de datos de matriz. Puede declarar una
matriz mediante la especificación del tipo de sus elementos. Si quiere que la matriz almacene elementos de
cualquier tipo, puede especificar object como su tipo. En el sistema de tipos unificado de C#, todos los tipos,
los predefinidos y los definidos por el usuario, los tipos de referencia y los tipos de valores, heredan directa o
indirectamente de Object.
type[] arrayName;
Ejemplo
Los ejemplos siguientes crean matrices unidimensionales, multidimensionales y escalonadas:
class TestArraysClass
{
static void Main()
{
// Declare a single-dimensional array of 5 integers.
int[] array1 = new int[5];
// Alternative syntax.
int[] array3 = { 1, 2, 3, 4, 5, 6 };
// Set the values of the first array in the jagged array structure.
jaggedArray[0] = new int[4] { 1, 2, 3, 4 };
}
}
int[] numbers = { 1, 2, 3, 4, 5 };
int lengthOfNumbers = numbers.Length;
La clase Array proporciona muchos otros métodos útiles y propiedades para ordenar, buscar y copiar matrices.
En los ejemplos siguientes se usa la propiedad Rank para mostrar el número de dimensiones de una matriz.
class TestArraysClass
{
static void Main()
{
// Declare and initialize an array.
int[,] theArray = new int[5, 10];
System.Console.WriteLine("The array has {0} dimensions.", theArray.Rank);
}
}
// Output: The array has 2 dimensions.
Vea también
Uso de matrices unidimensionales
Uso de matrices multidimensionales
Uso de matrices escalonadas
Utilizar foreach con matrices
Pasar matrices como argumentos
Matrices con tipo implícito
Guía de programación de C#
Colecciones
Para obtener más información, consulte la Especificación del lenguaje C#. La especificación del lenguaje es la
fuente definitiva de la sintaxis y el uso de C#.
Matrices unidimensionales (Guía de programación
de C#)
16/09/2021 • 2 minutes to read
Cree una matriz unidimensional mediante el operador new; para ello, especifique el tipo de elemento de matriz
y el número de elementos. En el ejemplo siguiente se declara una matriz de cinco enteros:
Esta matriz contiene los elementos de array[0] a array[4] . Los elementos de la matriz se inicializan en el valor
predeterminado del tipo de elemento, 0 para los enteros.
Las matrices pueden almacenar cualquier tipo de elemento que se especifique, como en el ejemplo siguiente, en
el que se declara una matriz de cadenas:
Inicialización de matriz
Puede inicializar los elementos de una matriz al declararla. El especificador de longitud no es necesario porque
la medida se infiere a partir del número de elementos de la lista de inicialización. Por ejemplo:
En el código siguiente se muestra una declaración de una matriz de cadenas donde cada elemento de la matriz
se inicializa mediante el nombre de un día:
string[] weekDays = new string[] { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
Puede evitar la expresión new y el tipo de matriz al inicializar una matriz en la declaración, tal como se muestra
en el código siguiente. Esto se conoce como matriz con tipo implícito:
int[] array2 = { 1, 3, 5, 7, 9 };
string[] weekDays2 = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
Puede declarar una variable de matriz sin crearla, pero debe usar el operador new cuando asigne una nueva
matriz a esta variable. Por ejemplo:
int[] array3;
array3 = new int[] { 1, 3, 5, 7, 9 }; // OK
//array3 = {1, 3, 5, 7, 9}; // Error
El resultado de esta instrucción depende de si SomeType es un tipo de valor o un tipo de referencia. Si es un tipo
de valor, la instrucción crea una matriz de 10 elementos, y cada uno de ellos tiene el tipo SomeType . Si SomeType
es un tipo de referencia, la instrucción crea una matriz de 10 elementos y cada uno de ellos se inicializa en una
referencia nula. En ambas instancias, los elementos se inicializan en el valor predeterminado para el tipo de
elemento. Para obtener más información sobre los tipos de valor y de referencia, consulte Tipos de valor y Tipos
de referencia.
Console.WriteLine(weekDays2[0]);
Console.WriteLine(weekDays2[1]);
Console.WriteLine(weekDays2[2]);
Console.WriteLine(weekDays2[3]);
Console.WriteLine(weekDays2[4]);
Console.WriteLine(weekDays2[5]);
Console.WriteLine(weekDays2[6]);
/*Output:
Sun
Mon
Tue
Wed
Thu
Fri
Sat
*/
Vea también
Array
Matrices
Matrices multidimensionales
Matrices escalonadas
Matrices multidimensionales (Guía de programación
de C#)
16/09/2021 • 2 minutes to read
Las matrices pueden tener varias dimensiones. Por ejemplo, la siguiente declaración crea una matriz
bidimensional de cuatro filas y dos columnas.
Inicialización de matriz
La matriz se puede inicializar en la declaración como se muestra en el ejemplo siguiente.
// Two-dimensional array.
int[,] array2D = new int[,] { { 1, 2 }, { 3, 4 }, { 5, 6 }, { 7, 8 } };
// The same array with dimensions specified.
int[,] array2Da = new int[4, 2] { { 1, 2 }, { 3, 4 }, { 5, 6 }, { 7, 8 } };
// A similar array with string elements.
string[,] array2Db = new string[3, 2] { { "one", "two" }, { "three", "four" },
{ "five", "six" } };
// Three-dimensional array.
int[,,] array3D = new int[,,] { { { 1, 2, 3 }, { 4, 5, 6 } },
{ { 7, 8, 9 }, { 10, 11, 12 } } };
// The same array with dimensions specified.
int[,,] array3Da = new int[2, 2, 3] { { { 1, 2, 3 }, { 4, 5, 6 } },
{ { 7, 8, 9 }, { 10, 11, 12 } } };
// Output:
// 1
// 2
// 3
// 4
// 7
// three
// 8
// 12
// 12 equals 12
int[,] array4 = { { 1, 2 }, { 3, 4 }, { 5, 6 }, { 7, 8 } };
Si opta por declarar una variable de matriz sin inicializarla, deberá usar el operador new para asignar una
matriz a la variable. El uso de new se muestra en el ejemplo siguiente.
int[,] array5;
array5 = new int[,] { { 1, 2 }, { 3, 4 }, { 5, 6 }, { 7, 8 } }; // OK
//array5 = {{1,2}, {3,4}, {5,6}, {7,8}}; // Error
array5[2, 1] = 25;
De igual forma, en el ejemplo siguiente se obtiene el valor de un elemento de matriz determinado y se asigna a
la variable elementValue .
El ejemplo de código siguiente inicializa los elementos de matriz con valores predeterminados (salvo las
matrices escalonadas).
Vea también
Guía de programación de C#
Matrices
Matrices unidimensionales
Matrices escalonadas
Matrices escalonadas (Guía de programación de
C#)
16/09/2021 • 3 minutes to read
Una matriz escalonada es una matriz cuyos elementos son matrices, posiblemente de diferentes tamaños. Una
matriz escalonada se denomina a veces "matriz de matrices". En los ejemplos siguientes, se muestra cómo
declarar, inicializar y acceder a matrices escalonadas.
La siguiente es una declaración de una matriz unidimensional que tiene tres elementos, cada uno de los cuales
es una matriz unidimensional de enteros:
Para poder usar jaggedArray , se deben inicializar sus elementos. Puede inicializar los elementos de esta forma:
Cada uno de los elementos es una matriz unidimensional de enteros. El primer elemento es una matriz de 5
enteros, el segundo es una matriz de 4 enteros y el tercero es una matriz de 2 enteros.
También es posible usar inicializadores para rellenar los elementos de matriz con valores, en cuyo caso no es
necesario el tamaño de la matriz. Por ejemplo:
Puede usar la siguiente forma abreviada. Tenga en cuenta que no puede omitir el operador new de la
inicialización de elementos porque no hay ninguna inicialización predeterminada para los elementos:
int[][] jaggedArray3 =
{
new int[] { 1, 3, 5, 7, 9 },
new int[] { 0, 2, 4, 6 },
new int[] { 11, 22 }
};
Una matriz escalonada es una matriz de matrices y, por consiguiente, sus elementos son tipos de referencia y se
inicializan en null .
Puede tener acceso a elementos individuales de la matriz como en estos ejemplos:
Puede tener acceso a los elementos individuales como se muestra en este ejemplo, que muestra el valor del
elemento [1,0] de la primera matriz (valor 5 ):
El método Length devuelve el número de matrices contenidos en la matriz escalonada. Por ejemplo,
suponiendo que ha declarado la matriz anterior, esta línea:
System.Console.WriteLine(jaggedArray4.Length);
devuelve un valor de 3.
Ejemplo
En este ejemplo, se crea una matriz cuyos elementos son matrices. Cada uno de los elementos de matriz tiene
un tamaño diferente.
class ArrayTest
{
static void Main()
{
// Declare the array of two elements.
int[][] arr = new int[2][];
Vea también
Array
Guía de programación de C#
Matrices
Matrices unidimensionales
Matrices multidimensionales
Uso de foreach con matrices (Guía de programación
de C#)
16/09/2021 • 2 minutes to read
La instrucción foreach ofrece una manera sencilla y limpia de iterar los elementos de una matriz.
Para matrices unidimensionales, la instrucción foreach procesa los elementos en orden creciente de índice,
comenzando con el índice 0 y terminando con el índice Length - 1 :
En el caso de matrices multidimensionales, los elementos se recorren de tal manera que primero se
incrementan los índices de la dimensión más a la derecha, luego la siguiente dimensión a la izquierda, y así
sucesivamente a la izquierda:
En cambio, con las matrices multidimensionales, usar un bucle for anidado ofrece un mayor control sobre el
orden en el que se procesan los elementos de la matriz.
Vea también
Array
Guía de programación de C#
Matrices
Matrices unidimensionales
Matrices multidimensionales
Matrices escalonadas
Pasar matrices como argumentos (Guía de
programación de C#)
16/09/2021 • 3 minutes to read
Las matrices se pueden pasar como argumentos a parámetros de método. Dado que las matrices son tipos de
referencia, el método puede cambiar el valor de los elementos.
int[] theArray = { 1, 3, 5, 7, 9 };
PrintArray(theArray);
Puede inicializar y pasar una nueva matriz en un solo paso, como se muestra en el ejemplo siguiente.
Ejemplo
En el ejemplo siguiente, una matriz de cadenas se inicializa y pasa como un argumento a un método
DisplayArray para cadenas. El método muestra los elementos de la matriz. A continuación, el método
ChangeArray invierte los elementos de la matriz y, después, el método ChangeArrayElements modifica los tres
primeros elementos de la matriz. Después de cada devolución de método, el método DisplayArray muestra que
pasar una matriz por valor no impide que se cambien los elementos de la matriz.
using System;
class ArrayExample
{
static void DisplayArray(string[] arr) => Console.WriteLine(string.Join(" ", arr));
int[,] theArray = { { 1, 2 }, { 2, 3 }, { 3, 4 } };
Print2DArray(theArray);
En el código siguiente, se muestra una declaración parcial de un método de impresión que acepta una matriz
bidimensional como su argumento.
void Print2DArray(int[,] arr)
{
// Method code.
}
Puede inicializar y pasar una matriz nueva en un solo paso, como se muestra en el ejemplo siguiente:
Ejemplo
En el ejemplo siguiente, una matriz bidimensional de enteros se inicializa y pasa al método Print2DArray . El
método muestra los elementos de la matriz.
class ArrayClass2D
{
static void Print2DArray(int[,] arr)
{
// Display the array elements.
for (int i = 0; i < arr.GetLength(0); i++)
{
for (int j = 0; j < arr.GetLength(1); j++)
{
System.Console.WriteLine("Element({0},{1})={2}", i, j, arr[i, j]);
}
}
}
static void Main()
{
// Pass the array as an argument.
Print2DArray(new int[,] { { 1, 2 }, { 3, 4 }, { 5, 6 }, { 7, 8 } });
Vea también
Guía de programación de C#
Matrices
Matrices unidimensionales
Matrices multidimensionales
Matrices escalonadas
Matrices con asignación implícita de tipos (Guía de
programación de C#)
16/09/2021 • 2 minutes to read
Puede crear una matriz con tipo implícito en la que se deduce el tipo de la instancia de matriz de los elementos
especificados en el inicializador de matriz. Las reglas de cualquier variable con tipo implícito también se aplican
a las matrices con tipo implícito. Para más información, vea Variables locales con asignación implícita de tipos.
Normalmente, se usan matrices con tipo implícito en expresiones de consulta junto con tipos anónimos e
inicializadores de objeto y colección.
En los ejemplos siguientes, se muestra cómo crear una matriz con tipo implícito:
class ImplicitlyTypedArraySample
{
static void Main()
{
var a = new[] { 1, 10, 100, 1000 }; // int[]
var b = new[] { "hello", null, "world" }; // string[]
En el ejemplo anterior observe que, con las matrices con tipo implícito, no se usan corchetes en el lado izquierdo
de la instrucción de inicialización. Tenga en cuenta también que las matrices escalonadas se inicializan mediante
new [] al igual que las matrices unidimensionales.
Vea también
Guía de programación de C#
Variables locales con asignación implícita de tipos
Matrices
Tipos anónimos
Inicializadores de objeto y colección
var
LINQ en C#
Cadenas (Guía de programación de C#)
16/09/2021 • 13 minutes to read
Una cadena es un objeto de tipo String cuyo valor es texto. Internamente, el texto se almacena como una
colección secuencial de solo lectura de objetos Char. No hay ningún carácter que finaliza en null al final de una
cadena de C#; por lo tanto, la cadena de C# puede contener cualquier número de caracteres nulos insertados
('\0'). La propiedad Length de una cadena representa el número de objetos Char que contiene, no el número de
caracteres Unicode. Para obtener acceso a los puntos de código Unicode individuales de una cadena, use el
objeto StringInfo.
// Initialize to null.
string message2 = null;
Tenga en cuenta que no se usa el operador new para crear un objeto de cadena, salvo cuando se inicialice la
cadena con una matriz de caracteres.
Inicialice una cadena con el valor constante Empty para crear un objeto String cuya cadena tenga longitud cero.
La representación literal de la cadena de una cadena de longitud cero es "". Mediante la inicialización de las
cadenas con el valor Empty en lugar de null, puede reducir las posibilidades de que se produzca una excepción
NullReferenceException. Use el método estático IsNullOrEmpty(String) para comprobar el valor de una cadena
antes de intentar obtener acceso a ella.
System.Console.WriteLine(s1);
// Output: A string is more than the sum of its chars.
Dado que una "modificación" de cadena es en realidad una creación de cadena, debe tener cuidado al crear
referencias a las cadenas. Si crea una referencia a una cadena y después "modifica" la cadena original, la
referencia seguirá apuntando al objeto original en lugar de al objeto nuevo creado al modificarse la cadena. El
código siguiente muestra este comportamiento:
System.Console.WriteLine(s2);
//Output: Hello
Para más información acerca de cómo crear cadenas nuevas basadas en modificaciones como las operaciones
de buscar y reemplazar en la cadena original, consulte Modificación del contenido de cadenas.
Utilice cadenas textuales para mayor comodidad y mejor legibilidad cuando el texto de la cadena contenga
caracteres de barra diagonal inversa, por ejemplo, en rutas de acceso de archivo. Como las cadenas textuales
conservan los caracteres de nueva línea como parte del texto de la cadena, pueden utilizarse para inicializar
cadenas multilíneas. Utilice comillas dobles para insertar una comilla simple dentro de una cadena textual. En el
ejemplo siguiente se muestran algunos usos habituales de las cadenas textuales:
\0 Null 0x0000
\a Alerta 0x0007
\b Retroceso 0x0008
WARNING
Cuando se usa la secuencia de escape \x y se especifican menos de 4 dígitos hexadecimales, si los caracteres que van
inmediatamente después de la secuencia de escape son dígitos hexadecimales válidos (es decir, 0-9, A-f y a-f), se
interpretará que forman parte de la secuencia de escape. Por ejemplo, \xA1 genera "¡", que es el punto de código
U+00A1. Sin embargo, si el carácter siguiente es "A" o "a", la secuencia de escape se interpretará como \xA1A y
producirá " ", que es el punto de código U+0A1A. En casos así, se pueden especificar los 4 dígitos hexadecimales (por
ejemplo, \x00A1 ) para evitar posibles errores de interpretación.
NOTE
En tiempo de compilación, las cadenas textuales se convierten en cadenas normales con las mismas secuencias de escape.
Por lo tanto, si se muestra una cadena textual en la ventana Inspección del depurador, verá los caracteres de escape
agregados por el compilador, no la versión textual del código fuente. Por ejemplo, la cadena textual @"C:\files.txt"
aparecerá en la ventana Inspección, como "C:\\files.txt".
Cadenas de formato
Una cadena de formato es una cadena cuyo contenido se determina de manera dinámica en tiempo de
ejecución. Las cadenas de formato se crean mediante la inserción de expresiones interpoladas o marcadores de
posición entre llaves dentro de una cadena. Todo lo incluido entre llaves ( {...} ) se resolverá en un valor y se
generará como una cadena con formato en tiempo de ejecución. Existen dos métodos para crear cadenas de
formato: interpolación de cadenas y formato compuesto.
Interpolación de cadenas
Disponible en C# 6.0 y versiones posteriores, las cadenas interpoladas se identifican por el carácter especial $ e
incluyen expresiones interpoladas entre llaves. Si no está familiarizado con la interpolación de cadenas, consulte
el tutorial interactivo Interpolación de cadenas en C# para obtener información general rápidamente.
Use la interpolación de cadenas para mejorar la legibilidad y el mantenimiento del código. Con la interpolación
de cadenas se obtienen los mismos resultados que con el método String.Format , pero mejora la facilidad de
uso y la claridad en línea.
var jh = (firstName: "Jupiter", lastName: "Hammon", born: 1711, published: 1761);
Console.WriteLine($"{jh.firstName} {jh.lastName} was an African American poet born in {jh.born}.");
Console.WriteLine($"He was first published in {jh.published} at the age of {jh.published - jh.born}.");
Console.WriteLine($"He'd be over {Math.Round((2018d - jh.born) / 100d) * 100d} years old today.");
// Output:
// Jupiter Hammon was an African American poet born in 1711.
// He was first published in 1761 at the age of 50.
// He'd be over 300 years old today.
A partir de C# 10, se puede utilizar la interpolación de cadenas para inicializar una cadena constante cuando
todas las expresiones utilizadas para los marcadores de posición son también cadenas constantes.
Formatos compuestos
String.Format emplea marcadores de posición entre llaves para crear una cadena de formato. Los resultados de
este ejemplo son similares a la salida del método de interpolación de cadenas usado anteriormente.
// Output:
// Phillis Wheatley was an African American poet born in 1753.
// She was first published in 1773 at the age of 20.
// She'd be over 300 years old today.
Para más información sobre cómo dar formato a los tipos .NET, consulte Aplicar formato a tipos en .NET.
Subcadenas
Una subcadena es cualquier secuencia de caracteres que se encuentra en una cadena. Use el método Substring
para crear una nueva cadena de una parte de la cadena original. Puede buscar una o más apariciones de una
subcadena con el método IndexOf. Use el método Replace para reemplazar todas las apariciones de una
subcadena especificada por una nueva cadena. Al igual que el método Substring, Replace devuelve una cadena
nueva y no modifica la cadena original. Para más información, consulte Cómo: Buscar cadenas y Procedimiento
para modificar el contenido de cadenas.
System.Console.WriteLine(s3.Replace("C#", "Basic"));
// Output: "Visual Basic Express"
Si el método String no proporciona la funcionalidad que debe tener para modificar los caracteres individuales
de una cadena, puede usar un objeto StringBuilder para modificar los caracteres individuales "en contexto" y,
después, crear una cadena para almacenar los resultados mediante el método StringBuilder. En el ejemplo
siguiente, se supone que debe modificar la cadena original de una manera determinada y, después, almacenar
los resultados para un uso futuro:
string question = "hOW DOES mICROSOFT wORD DEAL WITH THE cAPS lOCK KEY?";
System.Text.StringBuilder sb = new System.Text.StringBuilder(question);
string s = String.Empty;
En cambio, una cadena nula no hace referencia a una instancia de un objeto System.String y cualquier intento de
llamar a un método en una cadena nula produce una excepción NullReferenceException. Sin embargo, puede
utilizar cadenas nulas en operaciones de comparación y concatenación con otras cadenas. Los ejemplos
siguientes muestran algunos casos en que una referencia a una cadena nula provoca y no provoca una
excepción:
static void Main()
{
string str = "hello";
string nullStr = null;
string emptyStr = String.Empty;
// The null character can be displayed and counted, like other chars.
string s1 = "\x0" + "abc";
string s2 = "abc" + "\x0";
// Output of the following line: * abc*
Console.WriteLine("*" + s1 + "*");
// Output of the following line: *abc *
Console.WriteLine("*" + s2 + "*");
// Output of the following line: 4
Console.WriteLine(s2.Length);
}
En este ejemplo, se usa un objeto StringBuilder para crear una cadena a partir de un conjunto de tipos
numéricos:
using System;
using System.Text;
namespace CSRefStrings
{
class TestStringBuilder
{
static void Main()
{
var sb = new StringBuilder();
Temas relacionados
T EM A DESC RIP C IÓ N
Procedimiento para modificar el contenido de cadenas Muestra técnicas para transformar cadenas y modificar el
contenido de estas.
Análisis de cadenas mediante String.Split Contiene ejemplos de código que muestran cómo utilizar el
método String.Split para analizar cadenas.
Cómo: Buscar cadenas Explica cómo usar la búsqueda para especificar texto o
patrones en cadenas.
Determinación de si una cadena representa un valor Muestra cómo analizar de forma segura una cadena para ver
numérico si tiene un valor numérico válido.
T EM A DESC RIP C IÓ N
Operaciones básicas de cadenas Proporciona vínculos a temas que usan los métodos
System.String y System.Text.StringBuilder para realizar
operaciones básicas de cadenas.
Analizar cadenas de fecha y hora en .NET Muestra cómo convertir una cadena como "24/01/2008" en
un objeto System.DateTime.
Para determinar si una cadena es una representación válida de un tipo numérico especificado, use el método
estático TryParse implementado por todos los tipos numéricos primitivos y también por tipos como DateTime
y IPAddress. En el ejemplo siguiente se muestra cómo determinar si "108" es un valor int válido.
int i = 0;
string s = "108";
bool result = int.TryParse(s, out i); //i now = 108
Si la cadena contiene caracteres no numéricos o el valor numérico es demasiado grande o demasiado pequeño
para el tipo determinado que ha especificado, TryParse devuelve el valor false y establece el parámetro out en
cero. De lo contrario, devuelve el valor true y establece el parámetro out en el valor numérico de la cadena.
NOTE
Una cadena puede contener solamente caracteres numéricos pero no ser válida para el tipo cuyo método TryParse se
está usando. Por ejemplo, "256" no es un valor válido para byte pero sí para int . "98,6" no es un valor válido para
int pero sí para decimal .
Ejemplo
En los ejemplos siguientes se muestra cómo usar TryParse con representaciones de cadena de los valores
long , byte y decimal .
byte number2 = 0;
numString = "255"; // A value of 256 will return false
canConvert = byte.TryParse(numString, out number2);
if (canConvert == true)
Console.WriteLine("number2 now = {0}", number2);
else
Console.WriteLine("numString is not a valid byte");
decimal number3 = 0;
numString = "27.3"; //"27" is also a valid decimal
canConvert = decimal.TryParse(numString, out number3);
if (canConvert == true)
Console.WriteLine("number3 now = {0}", number3);
else
Console.WriteLine("number3 is not a valid decimal");
Programación sólida
Los tipos numéricos primitivos también implementan el método estático Parse , que produce una excepción si
la cadena no es un número válido. TryParse es, en general, más eficaz porque simplemente devuelve false si el
número no es válido.
Seguridad de .NET
Use siempre los métodos TryParse o Parse para validar los datos proporcionados por el usuario en controles
como cuadros de texto y cuadros combinados.
Vea también
Procedimiento Convertir una matriz de bytes en un valor int
Procedimiento Convertir una cadena en un número
Procedimiento Convertir cadenas hexadecimales en tipos numéricos
Análisis de cadenas numéricas
Aplicación de formato a tipos
Indizadores (Guía de programación de C#)
16/09/2021 • 3 minutes to read
Los indizadores permiten indizar las instancias de una clase o struct como matrices. El valor indizado se puede
establecer o recuperar sin especificar explícitamente un miembro de tipo o de instancia. Son similares a
propiedades, excepto en que sus descriptores de acceso usan parámetros.
En el ejemplo siguiente se define una clase genérica con métodos de descriptor de acceso get y set sencillos
para asignar y recuperar valores. La clase Program crea una instancia de esta clase para almacenar cadenas.
using System;
class SampleCollection<T>
{
// Declare an array to store the data elements.
private T[] arr = new T[100];
class Program
{
static void Main()
{
var stringCollection = new SampleCollection<string>();
stringCollection[0] = "Hello, World";
Console.WriteLine(stringCollection[0]);
}
}
// The example displays the following output:
// Hello, World.
NOTE
Para obtener más ejemplos, vea Secciones relacionadas.
class SampleCollection<T>
{
// Declare an array to store the data elements.
private T[] arr = new T[100];
int nextIndex = 0;
class Program
{
static void Main()
{
var stringCollection = new SampleCollection<string>();
stringCollection.Add("Hello, World");
System.Console.WriteLine(stringCollection[0]);
}
}
// The example displays the following output:
// Hello, World.
Tenga en cuenta que => presenta el cuerpo de la expresión y que la palabra clave get no se utiliza.
A partir de C# 7.0, los descriptores de acceso get y set se pueden implementar como miembros con forma de
expresión. En este caso, sí deben utilizarse las palabras clave get y set . Por ejemplo:
using System;
class SampleCollection<T>
{
// Declare an array to store the data elements.
private T[] arr = new T[100];
class Program
{
static void Main()
{
var stringCollection = new SampleCollection<string>();
stringCollection[0] = "Hello, World.";
Console.WriteLine(stringCollection[0]);
}
}
// The example displays the following output:
// Hello, World.
Información general sobre los indizadores
Los indizadores permiten indizar objetos de manera similar a como se hace con las matrices.
Un descriptor de acceso get devuelve un valor. Un descriptor de acceso set asigna un valor.
La palabra clave this se usa para definir los indizadores.
La palabra clave value se usa para definir el valor que va a asignar el descriptor de acceso set .
Los indizadores no tienen que ser indizados por un valor entero; depende de usted cómo definir el
mecanismo de búsqueda concreto.
Los indizadores se pueden sobrecargar.
Los indizadores pueden tener más de un parámetro formal, por ejemplo, al tener acceso a una matriz
bidimensional.
Secciones relacionadas
Utilizar indizadores
Indizadores en Interfaces
Comparación entre propiedades e indizadores
Restringir la accesibilidad del descriptor de acceso
Vea también
Guía de programación de C#
Propiedades
Uso de indizadores (Guía de programación de C#)
16/09/2021 • 6 minutes to read
Los indizadores son una comodidad sintáctica que le permiten crear una clase, estructura o interfaz a la que las
aplicaciones cliente pueden acceder como una matriz. El compilador generará una propiedad Item (o una
propiedad con otro nombre si está presente IndexerNameAttribute) y los métodos de descriptor de acceso
adecuados. Los indexadores se implementan con más frecuencia en tipos cuyo propósito principal consiste en
encapsular una matriz o colección interna. Por ejemplo, imagine que tiene una clase TempRecord que representa
la temperatura en grados Fahrenheit que se registra en 10 momentos diferentes durante un período de
24 horas. La clase contiene una matriz temps de tipo float[] para almacenar los valores de temperatura. Si
implementa un indizador en esta clase, los clientes pueden tener acceso a las temperaturas en una instancia de
TempRecord como float temp = tempRecord[4] en lugar de como float temp = tempRecord.temps[4] . La notación
del indizador no solo simplifica la sintaxis para las aplicaciones cliente; también hace que la clase y su finalidad
sean más intuitivas para que otros desarrolladores las entiendan.
Para declarar un indizador en una clase o un struct, use la palabra clave this, como en este ejemplo:
// Indexer declaration
public int this[int index]
{
// get and set accessors
}
IMPORTANT
Al declarar un indizador, se generará automáticamente una propiedad denominada Item en el objeto. No se puede
acceder directamente a la propiedad Item desde la instancia de expresión de acceso a miembros. Además, si agrega una
propiedad Item propia a un objeto con un indizador, obtendrá un error del compilador CS0102. Para evitar este error,
use IndexerNameAttribute para cambiar el nombre del indizador como se detalla a continuación.
Comentarios
Los tipos de un indexador y de sus parámetros deben ser al menos igual de accesibles que el propio indexador.
Para obtener más información sobre los niveles de accesibilidad, vea Modificadores de acceso.
Para obtener más información sobre cómo usar los indexadores con una interfaz, vea Indizadores en interfaces.
La firma de un indexador consta del número y los tipos de sus parámetros formales. No incluye el tipo de
indizador ni los nombres de los parámetros formales. Si declara más de un indexador en la misma clase, deben
tener firmas diferentes.
Un valor de indexador no está clasificado como una variable; por tanto, no se puede pasar un valor de indexador
como un parámetro ref u out.
Para proporcionar el indizador con un nombre que puedan usar otros lenguajes, use
System.Runtime.CompilerServices.IndexerNameAttribute, como se muestra en el ejemplo siguiente:
// Indexer declaration
[System.Runtime.CompilerServices.IndexerName("TheItem")]
public int this[int index]
{
// get and set accessors
}
Este indizador tendrá el nombre TheItem , ya que lo reemplaza el atributo de nombre del indizador. De forma
predeterminada, el nombre del indizador es Item .
Ejemplo 1
En el ejemplo siguiente, se muestra cómo declarar un campo de matriz privada, temps , como un indexador. El
indexador permite el acceso directo a la instancia tempRecord[i] . La alternativa a usar el indexador es declarar la
matriz como un miembro public y tener acceso directamente a sus miembros tempRecord.temps[i] .
// Indexer declaration.
// If index is out of range, the temps array will throw the exception.
public float this[int index]
{
get => temps[index];
set => temps[index] = value;
}
}
Tenga en cuenta que, cuando se evalúa el acceso de un indexador (por ejemplo, en una instrucción
Console.Write ), se invoca al descriptor de acceso get. Por tanto, si no hay ningún descriptor de acceso get , se
produce un error en tiempo de compilación.
using System;
class Program
{
static void Main()
{
var tempRecord = new TempRecord();
Ejemplo 2
En el ejemplo siguiente se declara una clase que almacena los días de la semana. Un descriptor de acceso get
toma una cadena, el nombre de un día, y devuelve el entero correspondiente. Por ejemplo, "Sunday" devuelve 0,
"Monday" devuelve 1 y así sucesivamente.
using System;
Ejemplo de consumo 2
using System;
class Program
{
static void Main(string[] args)
{
var week = new DayCollection();
Console.WriteLine(week["Fri"]);
try
{
Console.WriteLine(week["Made-up day"]);
}
catch (ArgumentOutOfRangeException e)
{
Console.WriteLine($"Not supported input: {e.Message}");
}
}
// Output:
// 5
// Not supported input: Day Made-up day is not supported.
// Day input must be in the form "Sun", "Mon", etc (Parameter 'day')
}
Ejemplo 3
En el ejemplo siguiente se declara una clase que almacena los días de la semana mediante la enumeración
System.DayOfWeek. Un descriptor de acceso get toma un valor DayOfWeek , el nombre de un día, y devuelve el
entero correspondiente. Por ejemplo, DayOfWeek.Sunday devuelve 0, DayOfWeek.Monday devuelve 1, y así
sucesivamente.
using System;
using Day = System.DayOfWeek;
class DayOfWeekCollection
{
Day[] days =
{
Day.Sunday, Day.Monday, Day.Tuesday, Day.Wednesday,
Day.Thursday, Day.Friday, Day.Saturday
};
Ejemplo de consumo 3
using System;
class Program
{
static void Main()
{
var week = new DayOfWeekCollection();
Console.WriteLine(week[DayOfWeek.Friday]);
try
{
Console.WriteLine(week[(DayOfWeek)43]);
}
catch (ArgumentOutOfRangeException e)
{
Console.WriteLine($"Not supported input: {e.Message}");
}
}
// Output:
// 5
// Not supported input: Day 43 is not supported.
// Day input must be a defined System.DayOfWeek value. (Parameter 'day')
}
Programación sólida
Hay dos formas principales en que se pueden mejorar la seguridad y confiabilidad de los indexadores:
Asegúrese de incorporar algún tipo de estrategia de control de errores para controlar la posibilidad de
que el código de cliente pase un valor de índice no válido. En el primer ejemplo de este tema, la clase
TempRecord proporciona una propiedad Length que permite al código de cliente comprobar la entrada
antes de pasarla al indexador. También puede colocar el código de control de errores en el propio
indexador. Asegúrese de documentar para los usuarios cualquier excepción que se produzca dentro de un
descriptor de acceso del indexador.
Establezca la accesibilidad de los descriptores de acceso get and set para que sea tan restrictiva como
razonable. Esto es importante para el descriptor de acceso set en particular. Para más información, vea
Restringir la accesibilidad del descriptor de acceso.
Vea también
Guía de programación de C#
Indizadores
Propiedades
Indizadores en interfaces (Guía de programación de
C#)
16/09/2021 • 2 minutes to read
Los indexadores se pueden declarar en una interfaz. Los descriptores de acceso de los indexadores de interfaz se
diferencian de los descriptores de acceso de los indexadores de clase de las maneras siguientes:
Los descriptores de acceso de interfaz no usan modificadores.
Normalmente, un descriptor de acceso de interfaz no tiene un cuerpo.
El propósito del descriptor de acceso es indicar si el indizador es de lectura y escritura, de solo lectura o de solo
escritura. Puede proporcionar una implementación para un indizador definido en una interfaz, pero esto es poco
frecuente. Los indizadores suelen definir una API para acceder a los campos de datos y los campos de datos no
se pueden definir en una interfaz.
A continuación tiene un ejemplo de un descriptor de acceso de indexador de interfaz:
// Indexer declaration:
string this[int index]
{
get;
set;
}
}
La firma de un indexador debe ser diferente de las firmas de los demás indexadores declarados en la misma
interfaz.
Ejemplo
En el siguiente ejemplo, se muestra cómo implementar indexadores de interfaz.
// Indexer on an interface:
public interface IIndexInterface
{
// Indexer declaration:
int this[int index]
{
get;
set;
}
}
/* Sample output:
Element #0 = 360877544
Element #1 = 327058047
Element #2 = 1913480832
Element #3 = 1519039937
Element #4 = 601472233
Element #5 = 323352310
Element #6 = 1422639981
Element #7 = 1797892494
Element #8 = 875761049
Element #9 = 393083859
*/
En el ejemplo anterior, podría usar la implementación del miembro de interfaz explícita al usar el nombre
completo del miembro de interfaz. Por ejemplo
En cambio, el nombre completo solo es necesario para evitar la ambigüedad cuando la clase implementa más
de una interfaz con la misma firma de indexador. Por ejemplo, si una clase Employee implementa dos interfaces
ICitizen y IEmployee y ambas interfaces tienen la misma firma de indexador, la implementación del miembro
de interfaz explícita es necesaria. Es decir, la siguiente declaración de indexador:
string IEmployee.this[int index]
{
}
Vea también
Guía de programación de C#
Indizadores
Propiedades
Interfaces
Comparación entre propiedades e indizadores (Guía
de programación de C#)
16/09/2021 • 2 minutes to read
Los indexadores son como propiedades. Excepto por las diferencias que se muestran en la tabla siguiente, todas
las reglas que se definen para los descriptores de acceso de propiedad se aplican también a los descriptores de
acceso de indexador.
P RO P IEDA D. IN DEXA DO R
Permite que los métodos se llamen como si fueran miembros Permite que se pueda tener acceso a los elementos de una
de datos públicos. colección interna de un objeto mediante la notación de
matriz en el propio objeto.
Un descriptor de acceso get de una propiedad no tiene Un descriptor de acceso get de un indexador tiene la
parámetros. misma lista de parámetros formales que el indexador.
Un descriptor de acceso set de una propiedad contiene el Un descriptor de acceso set de un indexador tiene la
parámetro value implícito. misma lista de parámetros formales que el indexador, y
también para el parámetro value.
Admite la sintaxis abreviada con Propiedades Admite miembros de cuerpo de expresión para obtener solo
autoimplementadas. indexadores.
Consulte también
Guía de programación de C#
Indizadores
Propiedades
Eventos (Guía de programación de C#)
16/09/2021 • 2 minutes to read
Cuando ocurre algo interesante, los eventos habilitan una clase u objeto para notificarlo a otras clases u objetos.
La clase que envía (o genera) el evento recibe el nombre de publicador y las clases que reciben (o controlan) el
evento se denominan suscriptores.
En una aplicación web o una aplicación de Windows Forms en C# típica, se puede suscribir a eventos generados
por controles, como botones y cuadros de lista. Puede usar el entorno de desarrollo integrado (IDE) de Visual C#
para examinar los eventos que publica un control y seleccionar los que quiera administrar. El IDE proporciona
una forma sencilla de agregar automáticamente un método de controlador de eventos vacío y el código para
suscribirse al evento. Para obtener más información, vea Procedimiento para suscribir y cancelar la suscripción a
eventos.
Secciones relacionadas
Para obtener más información, consulte:
Procedimiento para suscribir y cancelar la suscripción a eventos
Procedimiento para publicar eventos que cumplan las instrucciones de .NET
Procedimiento para generar eventos de una clase base en clases derivadas
Procedimiento para implementar eventos de interfaz
Procedimiento para implementar descriptores de acceso de eventos personalizados
Vea también
EventHandler
Guía de programación de C#
Delegados
Crear controladores de eventos en Windows Forms
Procedimiento Suscribir y cancelar la suscripción a
eventos (Guía de programación de C#)
16/09/2021 • 3 minutes to read
La suscripción a un evento publicado por otra clase se realiza cuando quiere escribir código personalizado al
que se llama cuando se produce ese evento. Por ejemplo, puede suscribirse al evento click de un botón para
que la aplicación realice alguna operación cuando el usuario haga clic en el botón.
Para suscribirse a eventos mediante el IDE de Visual Studio
1. Si no puede ver la ventana Propiedades , en la vista Diseño haga clic con el botón derecho en el
formulario o control para el que quiere crear un controlador de eventos y seleccione Propiedades .
2. En la parte superior de la ventana Propiedades , haga clic en el icono Eventos .
3. Haga doble clic en el evento que quiera crear, por ejemplo, el evento Load .
Visual C# crea un método de controlador de eventos vacío y lo agrega al código. También puede agregar
manualmente el código en la vista Código . Por ejemplo, las líneas siguientes de código declaran un
método de controlador de eventos al que se llamará cuando la clase Form genere el evento Load .
La línea de código que es necesaria para suscribirse al evento también se genera automáticamente con el
método InitializeComponent en el archivo Form1.Designer.cs del proyecto. Se parece a lo siguiente:
publisher.RaiseCustomEvent += HandleCustomEvent;
Observe que la sintaxis anterior es nueva en C# 2.0. Al igual que en la sintaxis de C# 1.0, el delegado
encapsulador debe crearse explícitamente mediante la palabra clave new :
También puede usar una expresión lambda para especificar un controlador de eventos:
public Form1()
{
InitializeComponent();
this.Click += (s,e) =>
{
MessageBox.Show(((MouseEventArgs)e).Location.ToString());
};
}
Es importante tener en cuenta que puede no resultar fácil cancelar la suscripción a un evento si se ha
usado una función anónima para suscribirse a él. Para cancelar la suscripción en esta situación, es
necesario regresar al código donde se ha suscrito al evento, almacenar el método anónimo en una
variable de delegado y, después, agregar el delegado al evento. En general, se recomienda que no use
funciones anónimas para suscribirse a eventos si va a tener que cancelar la suscripción al evento en el
código más adelante. Para obtener más información sobre las funciones anónimas, vea Funciones
anónimas.
publisher.RaiseCustomEvent -= HandleCustomEvent;
Cuando se haya cancelado la suscripción a un evento de todos los suscriptores, la instancia del evento en
la clase de editor se establecerá en null .
Vea también
Eventos
event
Procedimiento para publicar eventos que cumplan las instrucciones de .NET
Operadores - y -=
Operadores + y +=
Procedimiento Publicar eventos que cumplan las
directrices de .NET (guía de programación de C#)
16/09/2021 • 3 minutes to read
En el siguiente procedimiento se muestra cómo agregar eventos que cumplan el patrón de .NET estándar a las
clases y structs. Todos los eventos de la biblioteca de clases de .NET se basan en el delegado EventHandler, que
se define de la siguiente manera:
NOTE
.NET Framework 2.0 incluye una versión genérica de este delegado, EventHandler<TEventArgs>. En los siguientes
ejemplos se muestra cómo usar las dos versiones.
Aunque los eventos de las clases que defina se pueden basar en cualquier tipo de delegado válido, incluidos los
delegados que devuelven un valor, por lo general se recomienda que base los eventos en el patrón de .NET
mediante EventHandler, como se muestra en el ejemplo siguiente.
El nombre EventHandler puede dar lugar a un poco de confusión, ya que realmente no controla el evento.
EventHandler y EventHandler<TEventArgs> genérico son tipos de delegado. Un método o una expresión
lambda cuya firma coincide con la definición de delegado es el controlador de eventos y se invocará cuando se
genere el evento.
3. Declare el evento en la clase de publicación llevando a cabo uno de los siguientes pasos.
a. Si no tiene ninguna clase EventArgs personalizada, el tipo Event será el delegado EventHandler no
genérico. No es necesario declarar el delegado, porque ya está declarado en el espacio de
nombres System que se incluye al crear el proyecto de C#. Agregue el código siguiente a la clase
de publicador.
Ejemplo
En el siguiente ejemplo se muestran los pasos anteriores mediante el uso de una clase EventArgs personalizada
y EventHandler<TEventArgs> como tipo de evento.
using System;
namespace DotNetEvents
{
// Define a class to hold custom event info
public class CustomEventArgs : EventArgs
{
public CustomEventArgs(string message)
{
Message = message;
}
class Program
{
static void Main()
{
var pub = new Publisher();
var sub1 = new Subscriber("sub1", pub);
var sub2 = new Subscriber("sub2", pub);
Vea también
Delegate
Guía de programación de C#
Eventos
Delegados
Procedimiento Producir eventos de una clase base
en clases derivadas (Guía de programación de C#)
16/09/2021 • 3 minutes to read
En el siguiente ejemplo sencillo se muestra la forma estándar de declarar eventos en una clase base para que
también se puedan generar desde clases derivadas. Este patrón se usa mucho en las clases de Windows Forms
de las bibliotecas de clases de .NET.
Al crear una clase que se pueda usar como clase base para otras clases, debe considerar el hecho de que los
eventos son un tipo especial de delegado que solo se pueden invocar desde la clase que los haya declarado. Las
clases derivadas no pueden invocar directamente a eventos declarados en la clase base. Aunque a veces pueda
querer un evento que solo la clase base pueda generar, en la mayoría de los casos debería habilitar la clase
derivada para invocar a eventos de clase base. Para ello, puede crear un método de invocación protegido en la
clase base que encapsula el evento. Al llamar o invalidar a este método de invocación, las clases derivadas
pueden invocar directamente al evento.
NOTE
No declare eventos virtuales en una clase base y los invalide en una clase derivada. El compilador de C# no los controla
correctamente y no es posible decir si un suscriptor del evento derivado se está suscribiendo realmente al evento de clase
base.
Ejemplo
namespace BaseClassEvents
{
// Special EventArgs class to hold info about Shapes.
public class ShapeEventArgs : EventArgs
{
public ShapeEventArgs(double area)
{
NewArea = area;
}
// The event. Note that by using the generic EventHandler<T> event type
// we do not need to declare a separate delegate type.
public event EventHandler<ShapeEventArgs> ShapeChanged;
public ShapeContainer()
{
_list = new List<Shape>();
}
class Test
{
static void Main()
{
//Create the event publishers and subscriber
var circle = new Circle(54);
var rectangle = new Rectangle(12, 9);
var container = new ShapeContainer();
Un interfaz puede declarar un evento. En el siguiente ejemplo, se muestra cómo implementar eventos de
interfaz en una clase. Básicamente, las reglas son las mismas que para implementar cualquier propiedad o
método de interfaz.
namespace ImplementInterfaceEvents
{
public interface IDrawingObject
{
event EventHandler ShapeChanged;
}
public class MyEventArgs : EventArgs
{
// class members
}
public class Shape : IDrawingObject
{
public event EventHandler ShapeChanged;
void ChangeShape()
{
// Do something here before the event…
OnShapeChanged(new MyEventArgs(/*arguments*/));
Ejemplo
En el ejemplo siguiente se muestra cómo controlar la situación menos común en que la clase hereda de dos o
más interfaces y cada interfaz tiene un evento con el mismo nombre. En esta situación, debe proporcionar una
implementación de interfaz explícita para uno de los eventos como mínimo. Al escribir una implementación de
interfaz explícita para un evento, también debe escribir los descriptores de acceso del evento add y remove .
Normalmente, los proporciona el compilador, pero en este caso no es posible.
Al proporcionar sus propios descriptores de acceso, puede especificar si los dos eventos se representan
mediante el mismo evento en la clase o mediante eventos diferentes. Por ejemplo, si los eventos deben
provocarse en momentos diferentes según las especificaciones de la interfaz, puede asociar cada evento a una
implementación distinta en su clase. En el ejemplo siguiente, los suscriptores determinan qué evento OnDraw
recibirán al convertir la referencia de forma en IShape o en IDrawingObject .
namespace WrapTwoInterfaceEvents
{
using System;
Console.WriteLine("Drawing a shape.");
Consulte también
Guía de programación de C#
Eventos
Delegados
Implementación de interfaz explícita
Procedimiento para generar eventos de una clase base en clases derivadas
Procedimiento Implementar descriptores de acceso
de eventos personalizados (Guía de programación
de C#)
16/09/2021 • 2 minutes to read
Un evento es un tipo especial de delegado de multidifusión que solo puede invocarse desde dentro de la clase
en la que se declara. El código cliente se suscribe al evento proporcionando una referencia a un método que
debería invocarse cuando se desencadena el evento. Estos métodos se agregan a la lista de invocación del
delegado a través de descriptores de acceso de eventos, que son similares a los descriptores de acceso de
propiedad, con la diferencia de que los descriptores de acceso de eventos se denominan add y remove . En la
mayoría de los casos, no tiene que proporcionar descriptores de acceso de eventos personalizados. Cuando no
proporciona ningún descriptor de acceso de eventos personalizado en el código, el compilador los agrega
automáticamente. En cambio, en algunos casos puede que tenga que proporcionar un comportamiento
personalizado. Uno de estos casos se muestra en el tema Procedimiento Implementar eventos de interfaz.
Ejemplo
En el ejemplo siguiente se muestra cómo implementar descriptores de acceso de eventos add y remove
personalizados. Aunque puede sustituir cualquier código dentro de los descriptores de acceso, recomendamos
que bloquee el evento antes de agregar o quitar un nuevo método de control de eventos.
Vea también
Eventos
event
Parámetros de tipos genéricos (Guía de
programación de C#)
16/09/2021 • 2 minutes to read
En un tipo genérico o en una definición de método, un parámetro de tipo es un marcador de posición para un
tipo específico que un cliente especifica cuando crean instancias de una variable del tipo genérico. Una clase
genérica, como GenericList<T> que se muestra en Introducción a los genéricos, no puede usarse como está
porque no es realmente un tipo; es más parecida a un plano para un tipo. Para usar GenericList<T> , el código
cliente debe declarar y crear instancias de un tipo construido especificando un argumento de tipo dentro de
corchetes angulares. El argumento de tipo de esta clase determinada puede ser cualquier tipo reconocido por el
compilador. Puede crearse cualquier número de instancias de tipo construidas, usando cada una un argumento
de tipo diferente, de la manera siguiente:
En cada una de estas instancias de GenericList<T> , cada aparición de T en la clase se sustituye en tiempo de
ejecución con el argumento de tipo. Mediante esta sustitución, hemos creado tres objetos eficaces y con
seguridad de tipos independientes con una definición de clase única. Para obtener más información sobre cómo
se realiza esta sustitución mediante CLR, vea Genéricos en tiempo de ejecución.
Considere el uso de T como el nombre del parámetro de tipo para los tipos con un parámetro de tipo de
una sola letra.
Consulte también
System.Collections.Generic
Guía de programación de C#
Genéricos
Diferencias entre plantillas de C++ y tipos genéricos de C#
Restricciones de tipos de parámetros (Guía de
programación de C#)
16/09/2021 • 12 minutes to read
Las restricciones informan al compilador sobre las capacidades que debe tener un argumento de tipo. Sin
restricciones, el argumento de tipo puede ser cualquier tipo. El compilador solo puede suponer los miembros de
System.Object, que es la clase base fundamental de los tipos .NET. Para más información, vea Por qué usar
restricciones. Si el código de cliente usa un tipo que no cumple una restricción, el compilador emite un error. Las
restricciones se especifican con la palabra clave contextual where . En la tabla siguiente se muestran los distintos
tipos de restricciones:
where T : notnull El argumento de tipo debe ser un tipo que no acepta valores
NULL. El argumento puede ser un tipo de referencia que no
acepta valores NULL en C# 8.0 o posterior, o bien un tipo de
valor que no acepta valores NULL.
where T : <base class name> El argumento de tipo debe ser o derivarse de la clase base
especificada. En un contexto que admite un valor NULL en
C# 8.0 y versiones posteriores, T debe ser un tipo de
referencia que no acepta valores NULL derivado de la clase
base especificada.
where T : <base class name>? El argumento de tipo debe ser o derivarse de la clase base
especificada. En un contexto que admite un valor NULL en
C# 8.0 y versiones posteriores, T puede ser un tipo que
acepta o no acepta valores NULL derivado de la clase base
especificada.
public T FindFirstOccurrence(string s)
{
Node current = head;
T t = null;
La restricción permite que la clase genérica use la propiedad Employee.Name . La restricción especifica que está
garantizado que todos los elementos de tipo T sean un objeto Employee u objeto que hereda de Employee .
Pueden aplicarse varias restricciones en el mismo parámetro de tipo, y las propias restricciones pueden ser tipos
genéricos, de la manera siguiente:
class EmployeeList<T> where T : Employee, IEmployee, System.IComparable<T>, new()
{
// ...
}
Al aplicar la restricción where T : class , evite los operadores == y != en el parámetro de tipo porque estos
operadores se probarán solo para la identidad de referencia, no para la igualdad de valor. Este comportamiento
se produce incluso si estos operadores están sobrecargados en un tipo que se usa como un argumento. En el
código siguiente se ilustra este punto; el resultado es False incluso cuando la clase String sobrecarga al operador
== .
El compilador solo sabe que T es un tipo de referencia en tiempo de compilación y debe usar los operadores
predeterminados que son válidos para todos los tipos de referencia. Si debe probar la igualdad de valor, la
manera recomendada también es aplicar la restricción where T : IEquatable<T> o where T : IComparable<T> e
implementar esa interfaz en cualquier clase que se usará para construir la clase genérica.
class Base { }
class Test<T, U>
where U : struct
where T : Base, new()
{ }
En el ejemplo anterior, T es una restricción de tipo en el contexto del método Add , y un parámetro de tipo sin
enlazar en el contexto de la clase List .
Los parámetros de tipo también pueden usarse como restricciones en definiciones de clase genéricas. El
parámetro de tipo debe declararse dentro de los corchetes angulares junto con los demás parámetros de tipo:
La utilidad de los parámetros de tipo como restricciones con clases genéricas es limitada, ya que el compilador
no puede dar por supuesto nada sobre el parámetro de tipo, excepto que deriva de System.Object . Use
parámetros de tipo como restricciones en clases genéricas en escenarios en los que quiere aplicar una relación
de herencia entre dos parámetros de tipo.
Restricción notnull
A partir C# 8.0, puede usar la restricción notnull para especificar que el argumento de tipo debe ser un tipo de
valor que no acepta valores NULL o un tipo de referencia que no acepta valores NULL. A diferencia de la
mayoría de las demás restricciones, si un argumento de tipo infringe la restricción notnull , el compilador
genera una advertencia en lugar de un error.
La restricción notnull tiene efecto solo cuando se usa en un contexto que admite un valor NULL. Si agrega la
restricción notnull en un contexto en el que se desconocen los valores NULL, el compilador no genera
advertencias ni errores para las infracciones de la restricción.
Restricción class
A partir de C# 8.0, la restricción class en un contexto que admite un valor NULL especifica que el argumento
de tipo debe ser un tipo de referencia que no acepta valores NULL. En un contexto que admite un valor NULL,
cuando un argumento de tipo es un tipo de referencia que admite un valor NULL, el compilador genera una
advertencia.
Restricción default
La incorporación de tipos de referencia que aceptan valores NULL complica el uso de T? en un método o tipo
genérico. Antes de C# 8, T? solo se podía usar cuando la restricción struct se aplicaba a T . En ese contexto,
T? hace referencia al tipo Nullable<T> para T . A partir de C# 8, T? se podría usar con la restricción struct
o class , pero una de ellas debe estar presente. Cuando se ha usado la restricción class , T? se refiere al tipo
de referencia que acepta valores NULL para T . A partir de C# 9, T? se puede usar cuando no se aplica ninguna
restricción. En ese caso, T? tiene la misma interpretación que en C# 8 para tipos de valor y tipos de referencia.
Sin embargo, si T es una instancia de Nullable<T>, T? es igual que T . En otras palabras, no se convierte en
T?? .
Dado que T? ahora se puede usar sin las restricciones class o struct , pueden surgir ambigüedades en
invalidaciones o implementaciones de interfaz explícitas. En ambos casos, la invalidación no incluye las
restricciones, pero las hereda de la clase base. Cuando la clase base no aplica las restricciones class o struct ,
las clases derivadas deben especificar de algún modo que una invalidación se aplica al método base sin ninguna
restricción. Ahí es cuando el método derivado aplica la restricción default . La restricción default no aclara ni
la restricción class ni la struct .
Restricción no administrada
A partir de C# 7.3, puede usar la restricción unmanaged para especificar que el parámetro de tipo debe ser un
tipo no administrado que no acepta valores NULL. La restricción unmanaged permite escribir rutinas reutilizables
para trabajar con tipos que se pueden manipular como bloques de memoria, como se muestra en el ejemplo
siguiente:
El método anterior debe compilarse en un contexto unsafe , ya que usa el operador sizeof en un tipo que se
desconoce si es integrado. Sin la restricción unmanaged , el operador sizeof no está disponible.
La restricción unmanaged implica la restricción struct y no se puede combinar con ella. Dado que la restricción
struct implica la restricción new() , la restricción unmanaged tampoco se puede combinar con la restricción
new() .
Restricciones de delegado
También a partir de C# 7.3, puede usar System.Delegate o System.MulticastDelegate como una restricción de
clase base. CLR siempre permitía esta restricción, pero el lenguaje C# no la permitía. La restricción
System.Delegate permite escribir código que funciona con los delegados en un modo con seguridad de tipos.
En el código siguiente se define un método de extensión que combina dos delegados siempre y cuando sean del
mismo tipo:
Puede usar el método anterior para combinar delegados que sean del mismo tipo:
Si quita la marca de comentario de la última línea, no se compilará. Tanto first como test son tipos de
delegado, pero son tipos de delegado distintos.
Restricciones de enumeración
A partir de C# 7.3, también puede especificar el tipo System.Enum como una restricción de clase base. CLR
siempre permitía esta restricción, pero el lenguaje C# no la permitía. Los genéricos que usan System.Enum
proporcionan programación con seguridad de tipos para almacenar en caché los resultados de usar los
métodos estáticos en System.Enum . En el ejemplo siguiente se buscan todos los valores válidos para un tipo de
enumeración y, después, se compila un diccionario que asigna esos valores a su representación de cadena.
Enum.GetValues y Enum.GetName usan reflexión, lo que tiene consecuencias en el rendimiento. Puede llamar a
EnumNamedValues para compilar una recopilación que se almacene en caché y se vuelva a usar, en lugar de
repetir las llamadas que requieren reflexión.
Podría usarla como se muestra en el ejemplo siguiente para crear una enumeración y compilar un diccionario
con sus nombres y valores:
enum Rainbow
{
Red,
Orange,
Yellow,
Green,
Blue,
Indigo,
Violet
}
Vea también
System.Collections.Generic
Guía de programación de C#
Introducción a los genéricos
Clases genéricas
new (restricción)
Clases genéricas (Guía de programación de C#)
16/09/2021 • 4 minutes to read
Las clases genéricas encapsulan operaciones que no son específicas de un tipo de datos determinado. El uso
más común de las clases genéricas es con colecciones como listas vinculadas, tablas hash, pilas, colas y árboles,
entre otros. Las operaciones como la adición y eliminación de elementos de la colección se realizan básicamente
de la misma manera independientemente del tipo de datos que se almacenan.
Para la mayoría de los escenarios que necesitan clases de colección, el enfoque recomendado es usar las que se
proporcionan en la biblioteca de clases .NET. Para más información sobre el uso de estas clases, vea Colecciones
genéricas en .NET.
Normalmente, crea clases genéricas empezando con una clase concreta existente, y cambiando tipos en
parámetros de tipo de uno en uno hasta que alcanza el equilibrio óptimo de generalización y facilidad de uso. Al
crear sus propias clases genéricas, entre las consideraciones importantes se incluyen las siguientes:
Los tipos que se van a generalizar en parámetros de tipo.
Como norma, cuantos más tipos pueda parametrizar, más flexible y reutilizable será su código. En
cambio, demasiada generalización puede crear código que sea difícil de leer o entender para otros
desarrolladores.
Las restricciones, si existen, que se van a aplicar a los parámetros de tipo (Vea Restricciones de
parámetros de tipo).
Una buena norma es aplicar el máximo número de restricciones posible que todavía le permitan tratar
los tipos que debe controlar. Por ejemplo, si sabe que su clase genérica está diseñada para usarse solo
con tipos de referencia, aplique la restricción de clase. Esto evitará el uso no previsto de su clase con tipos
de valor, y le permitirá usar el operador as en T , y comprobar si hay valores NULL.
Si separar el comportamiento genérico en clases base y subclases.
Como las clases genéricas pueden servir como clases base, las mismas consideraciones de diseño se
aplican aquí con clases no genéricas. Vea las reglas sobre cómo heredar de clases base genéricas
posteriormente en este tema.
Si implementar una o más interfaces genéricas.
Por ejemplo, si está diseñando una clase que se usará para crear elementos en una colección basada en
genéricos, puede que tenga que implementar una interfaz como IComparable<T> donde T es el tipo de
su clase.
Para obtener un ejemplo de una clase genérica simple, vea Introducción a los genéricos.
Las reglas para los parámetros de tipo y las restricciones tienen varias implicaciones para el comportamiento de
clase genérico, especialmente respecto a la herencia y a la accesibilidad de miembros. Antes de continuar, debe
entender algunos términos. Para una clase genérica Node<T>, , el código de cliente puede hacer referencia a la
clase especificando un argumento de tipo, para crear un tipo construido cerrado ( Node<int> ). De manera
alternativa, puede dejar el parámetro de tipo sin especificar, por ejemplo cuando especifica una clase base
genérica, para crear un tipo construido abierto ( Node<T> ). Las clases genéricas pueden heredar de determinadas
clases base construidas abiertas o construidas cerradas:
class BaseNode { }
class BaseNodeGeneric<T> { }
// concrete type
class NodeConcrete<T> : BaseNode { }
Las clases no genéricas, en otras palabras, concretas, pueden heredar de clases base construidas cerradas, pero
no desde clases construidas abiertas ni desde parámetros de tipo porque no hay ninguna manera en tiempo de
ejecución para que el código de cliente proporcione el argumento de tipo necesario para crear instancias de la
clase base.
//No error
class Node1 : BaseNodeGeneric<int> { }
//Generates an error
//class Node2 : BaseNodeGeneric<T> {}
//Generates an error
//class Node3 : T {}
Las clases genéricas que heredan de tipos construidos abiertos deben proporcionar argumentos de tipo para
cualquier parámetro de tipo de clase base que no se comparta mediante la clase heredada, como se demuestra
en el código siguiente:
//No error
class Node4<T> : BaseNodeMultiple<T, int> { }
//No error
class Node5<T, U> : BaseNodeMultiple<T, U> { }
//Generates an error
//class Node6<T> : BaseNodeMultiple<T, U> {}
Las clases genéricas que heredan de tipos construidos abiertos deben especificar restricciones que son un
superconjunto de las restricciones del tipo base, o que las implican:
Los tipos genéricos pueden usar varios parámetros de tipo y restricciones, de la manera siguiente:
Tipos construidos cerrados y construidos abiertos pueden usarse como parámetros de método:
void Swap<T>(List<T> list1, List<T> list2)
{
//code to swap items
}
Si una clase genérica implementa una interfaz, todas las instancias de esa clase se pueden convertir en esa
interfaz.
Las clases genéricas son invariables. En otras palabras, si un parámetro de entrada especifica un
List<BaseClass> , obtendrá un error en tiempo de compilación si intenta proporcionar un List<DerivedClass> .
Consulte también
System.Collections.Generic
Guía de programación de C#
Genéricos
Guardar el estado de los enumeradores
Un puzle de herencia, parte uno
Interfaces genéricas (Guía de programación de C#)
16/09/2021 • 4 minutes to read
A menudo es útil definir interfaces para las clases de colección genéricas o para las clases genéricas que
representan los elementos de la colección. Lo preferible para las clases genéricas es usar interfaces genéricas,
como IComparable<T> en lugar de IComparable, para evitar las operaciones de conversión boxing y unboxing
en los tipos de valor. En la biblioteca de clases de .NET se definen varias interfaces genéricas para usarlas con las
clases de colección del espacio de nombres System.Collections.Generic.
Cuando una interfaz se especifica como restricción en un parámetro de tipo, solo se pueden usar los tipos que
implementan la interfaz. El ejemplo de código siguiente muestra una clase SortedList<T> derivada de la clase
GenericList<T> . Para obtener más información, vea Introducción a los genéricos. SortedList<T> agrega la
restricción where T : IComparable<T> . Esto permite al método BubbleSort de SortedList<T> usar el método
CompareTo genérico con los elementos de lista. En este ejemplo, los elementos de lista son una clase simple,
Person , que implementa IComparable<Person> .
do
{
Node previous = null;
Node current = head;
swapped = false;
if (previous == null)
{
head = tmp;
}
else
{
previous.next = tmp;
}
previous = tmp;
swapped = true;
}
else
{
previous = current;
current = current.next;
}
}
} while (swapped);
}
}
int[] ages = new int[] { 45, 19, 28, 23, 18, 9, 108, 72, 30, 35 };
Se pueden especificar varias interfaces como restricciones en un solo tipo, de la siguiente manera:
Las reglas de herencia que se aplican a las clases también se aplican a las interfaces:
interface IMonth<T> { }
Las interfaces genéricas pueden heredar de interfaces no genéricas si son covariantes, lo que significa que solo
usan su parámetro de tipo como valor devuelto. En la biblioteca de clases de .NET, IEnumerable<T> hereda de
IEnumerable porque IEnumerable<T> solo usa T en el valor devuelto de GetEnumerator y en el captador de
propiedad Current.
Las clases concretas pueden implementar interfaces construidas cerradas, de la siguiente manera:
interface IBaseInterface<T> { }
Las clases genéricas pueden implementar interfaces genéricas o interfaces construidas cerradas siempre que la
lista de parámetros de la clase suministre todos los argumentos que necesita la interfaz, de la siguiente manera:
interface IBaseInterface1<T> { }
interface IBaseInterface2<T, U> { }
Las reglas que controlan la sobrecarga de métodos son las mismas para los métodos incluidos en las clases
genéricas, los structs genéricos o las interfaces genéricas. Para obtener más información, vea Métodos
genéricos.
Consulte también
Guía de programación de C#
Introducción a los genéricos
interface
Genéricos
Métodos genéricos (Guía de programación de C#)
16/09/2021 • 2 minutes to read
Un método genérico es un método que se declara con parámetros de tipo, de la manera siguiente:
En el siguiente ejemplo de código se muestra una manera de llamar al método con int para el argumento de
tipo:
También puede omitir el argumento de tipo y el compilador lo deducirá. La siguiente llamada a Swap es
equivalente a la llamada anterior:
Las mismas reglas para la inferencia de tipos se aplican a los métodos estáticos y a los métodos de instancia. El
compilador puede deducir los parámetros de tipo basados en los argumentos de método que se pasan; no
puede deducir los parámetros de tipo solo desde un valor devuelto o una restricción. Por lo tanto, la inferencia
de tipos no funciona con métodos que no tienen parámetros. La inferencia de tipos se produce en tiempo de
compilación antes de que el compilador intente resolver las firmas de método sobrecargadas. El compilador
aplica la lógica de inferencia de tipos a todos los métodos genéricos que comparten el mismo nombre. En el
paso de resolución de sobrecarga, el compilador incluye solo esos métodos genéricos en los que la inferencia de
tipos se ha realizado correctamente.
Dentro de una clase genérica, los métodos no genéricos pueden tener acceso a los parámetros de tipo de nivel
de clase, de la manera siguiente:
class SampleClass<T>
{
void Swap(ref T lhs, ref T rhs) { }
}
Si define un método genérico que toma los mismos parámetros de tipo que la clase contenedora, el compilador
genera la advertencia CS0693 porque, dentro del ámbito del método, el argumento que se ha proporcionado
para el T interno oculta el argumento que se ha proporcionado para el T externo. Si necesita la flexibilidad de
llamar a un método de la clase genérica con argumentos de tipo diferentes de los que se han proporcionado
cuando se ha creado una instancia de la clase, considere la posibilidad de proporcionar otro identificador para el
parámetro de tipo del método, como se muestra en GenericList2<T> en el ejemplo siguiente.
class GenericList<T>
{
// CS0693
void SampleMethod<T>() { }
}
class GenericList2<T>
{
//No warning
void SampleMethod<U>() { }
}
Use restricciones para permitir operaciones más especializadas en parámetros de tipo de métodos. Esta versión
de Swap<T> , ahora denominada SwapIfGreater<T> , solo puede usarse con argumentos de tipo que implementan
IComparable<T>.
Los métodos genéricos pueden sobrecargarse en varios parámetros de tipo. Por ejemplo, todos los métodos
siguientes pueden ubicarse en la misma clase:
void DoWork() { }
void DoWork<T>() { }
void DoWork<T, U>() { }
Vea también
System.Collections.Generic
Guía de programación de C#
Introducción a los genéricos
Métodos
Genéricos y matrices (Guía de programación de C#)
16/09/2021 • 2 minutes to read
En C# 2.0 y versiones posteriores, las matrices unidimensionales que tienen un límite inferior de cero
implementan IList<T> automáticamente. Esto le permite crear métodos genéricos que pueden usar el mismo
código para recorrer en iteración matrices y otros tipos de colección. Esta técnica es útil principalmente para leer
datos en colecciones. La interfaz IList<T> no puede usarse para agregar o quitar elementos de una matriz. Se
generará una excepción si intenta llamar a un método IList<T> como RemoveAt en una matriz en este contexto.
En el siguiente ejemplo de código se muestra cómo un método genérico único que toma un parámetro de
entrada IList<T> puede recorrer en iteración una lista y una matriz, en este caso una matriz de enteros.
class Program
{
static void Main()
{
int[] arr = { 0, 1, 2, 3, 4 };
List<int> list = new List<int>();
ProcessItems<int>(arr);
ProcessItems<int>(list);
}
Consulte también
System.Collections.Generic
Guía de programación de C#
Genéricos
Matrices
Genéricos
Delegados genéricos (Guía de programación de C#)
16/09/2021 • 2 minutes to read
Un delegado puede definir sus propios parámetros de tipo. El código que hace referencia al delegado genérico
puede especificar el tipo de argumento para crear un tipo construido abierto, igual que al crear una instancia de
una clase genérica o al llamar a un método genérico, como se muestra en el siguiente ejemplo:
C# 2.0 tiene una nueva característica denominada conversión de grupo de métodos, que se aplica a los tipos
delegados concretos y genéricos, y permite escribir la línea anterior con esta sintaxis simplificada:
Del<int> m2 = Notify;
Los delegados definidos dentro de una clase genérica pueden usar los parámetros de tipo de la clase genérica
de la misma manera que lo hacen los métodos de clase.
class Stack<T>
{
T[] items;
int index;
El código que hace referencia al delegado debe especificar el argumento de tipo de la clase contenedora, de la
siguiente manera:
Los delegados genéricos son especialmente útiles para definir eventos basados en el patrón de diseño habitual
porque el argumento del remitente puede estar fuertemente tipado y ya no tiene que convertirse a y de Object.
delegate void StackEventHandler<T, U>(T sender, U eventArgs);
class Stack<T>
{
public class StackEventArgs : System.EventArgs { }
public event StackEventHandler<Stack<T>, StackEventArgs> stackEvent;
class SampleClass
{
public void HandleStackChange<T>(Stack<T> stack, Stack<T>.StackEventArgs args) { }
}
Consulte también
System.Collections.Generic
Guía de programación de C#
Introducción a los genéricos
Métodos genéricos
Clases genéricas
Interfaces genéricas
Delegados
Genéricos
Diferencias entre plantillas de C++ y tipos genéricos
de C# (Guía de programación de C#)
16/09/2021 • 2 minutes to read
Los tipos genéricos de C# y las plantillas de C++ son dos características de lenguaje que ofrecen compatibilidad
con tipos parametrizados. Pero existen muchas diferencias entre ambos. En el nivel de sintaxis, los tipos
genéricos de C# resultan un enfoque más sencillo que los tipos parametrizados sin la complejidad de las
plantillas de C++. Además, C# no intenta ofrecer toda la funcionalidad que ofrecen las plantillas de C++. En el
nivel de implementación, la principal diferencia es que las sustituciones de tipo genérico de C# se realizan en
tiempo de ejecución y, por tanto, se conserva la información de tipo genérico para los objetos con instancias.
Para obtener más información, vea Genéricos en el motor en tiempo de ejecución.
Estas son las principales diferencias entre plantillas de C++ y tipos genéricos de C#:
Los tipos genéricos de C# no ofrecen tanta flexibilidad como las plantillas de C++. Por ejemplo, no es
posible llamar a operadores aritméticos en una clase de tipos genéricos de C#, aunque es posible llamar
a operadores definidos por el usuario.
C# no permite parámetros de plantilla sin tipo, como template C<int i> {} .
C# no admite la especialización explícita; es decir, una implementación personalizada de una plantilla para
un tipo específico.
C# no admite la especialización parcial: una implementación personalizada de un subconjunto de los
argumentos de tipo.
C# no permite que el parámetro de tipo se use como clase base del tipo genérico.
C# no permite que los parámetros de tipo tengan tipos predeterminados.
En C#, un parámetro de tipo genérico no puede ser un tipo genérico, aunque se pueden usar tipos
construidos como genéricos. C++ permite parámetros de plantilla.
C++ permite código que podría no ser válido para todos los parámetros de tipo en la plantilla, que luego
busca el tipo específico que se usa como parámetro de tipo. C# requiere que el código de una clase se
escriba de manera que funcione con cualquier tipo que cumpla las restricciones. Por ejemplo, en C++ es
posible escribir una función que use los operadores aritméticos + y - en objetos del parámetro de tipo,
lo que producirá un error en el momento de creación de instancias de la plantilla con un tipo que no
admita estos operadores. C# no permite esto; las únicas construcciones de lenguaje permitidas son las
que se pueden deducir de las restricciones.
Consulte también
Guía de programación de C#
Introducción a los genéricos
Templates (Plantillas [C++])
Genéricos en el motor en tiempo de ejecución (Guía
de programación de C#)
16/09/2021 • 3 minutes to read
Cuando se compila un tipo o método genérico en el lenguaje intermedio de Microsoft (MSIL), contiene
metadatos que lo identifican como poseedor de parámetros de tipo. La forma en que se usa MSIL para un tipo
genérico depende de si el parámetro de tipo proporcionado es un tipo de valor o de referencia.
Cuando se construye por primera vez un tipo genérico con un tipo de valor como parámetro, el motor de
ejecución crea un tipo genérico especializado sustituyendo el parámetro o los parámetros proporcionados en
los lugares adecuados del MSIL. Los tipos genéricos especializados se crean una vez para cada tipo de valor
único que se usa como parámetro.
Por ejemplo, suponga que el código de su programa ha declarado una pila compuesta por enteros:
Stack<int> stack;
En este momento, el motor de ejecución genera una versión especializada de la clase Stack<T> sustituyendo el
entero adecuadamente para su parámetro. Ahora, cada vez que el código de su programa use una pila de
enteros, el motor en tiempo de ejecución vuelve a usar la clase especializada generada Stack<T>. En el ejemplo
siguiente, se crean dos instancias de una pila de enteros y comparten una instancia única del código Stack<int>
:
En cambio, suponga que otra clase Stack<T> con un tipo de valor diferente, como un long o una estructura
definida por el usuario como parámetro, se crea en otro punto del código. Como resultado, el motor de
ejecución genera otra versión del tipo genérico y sustituye un long en los lugares apropiados en el MSIL. Las
conversiones ya no son necesarias porque cada clase genérica especializada contiene de forma nativa el tipo de
valor.
Los genéricos funcionan de forma ligeramente distinta para los tipos de referencia. Cuando se construye un tipo
genérico por primera vez con un tipo de referencia, el motor en tiempo de ejecución crea un tipo genérico
especializado sustituyendo las referencias a objetos para los parámetros del MSIL. Después, cada vez que se
crea una instancia de un tipo construido con un tipo de referencia como parámetro, independientemente del
tipo que sea, el motor de ejecución vuelve a usar la versión especializada del tipo genérico previamente creada.
Esto es posible porque todas las referencias son del mismo tamaño.
Por ejemplo, suponga que tiene dos tipos de referencia, una clase Customer y una clase Order , y que ha creado
una pila de tipos Customer :
class Customer { }
class Order { }
Stack<Customer> customers;
En este punto, el motor de ejecución genera una versión especializada de la clase Stack<T> que, en lugar de
almacenar los datos, almacena referencias a objetos que se rellenarán más tarde. Suponga que la línea siguiente
de código crea una pila de otro tipo de referencia, que se denomina Order :
A diferencia de lo que sucede con los tipos de valor, no se crea otra versión especializada de la clase Stack<T>
para el tipo Order . En su lugar, se crea una instancia de la versión especializada de la clase Stack<T> y se
establece la variable orders para hacer referencia a ella. Suponga que encuentra una línea de código para crear
una pila de tipo Customer :
Como con el uso anterior de la clase Stack<T> creada usando el tipo Order , se crea otra instancia de la clase
especializada Stack<T>. Los punteros contenidos allí se establecen para hacer referencia a un área de memoria
del tamaño de un tipo Customer . Dado que el número de tipos de referencia puede variar significativamente de
un programa a otro, la implementación de genéricos de C# reduce significativamente la cantidad de código
limitando a uno el número de clases especializadas creadas por el compilador para las clases genéricas de tipos
de referencia.
Además, cuando se crea una instancia de una clase de C# genérica mediante un parámetro de tipo de valor o de
referencia se puede consultar en tiempo de ejecución mediante reflexión, y se puede comprobar tanto su tipo
real como su parámetro de tipo.
Consulte también
System.Collections.Generic
Guía de programación de C#
Introducción a los genéricos
Genéricos
Genéricos y reflexión (Guía de programación de C#)
16/09/2021 • 3 minutes to read
Dado que Common Language Runtime (CLR) tiene acceso a la información de tipos genéricos en tiempo de
ejecución, se puede usar la reflexión para obtener información sobre los tipos genéricos de la misma manera
que para los tipos no genéricos. Para obtener más información, vea Genéricos en el motor en tiempo de
ejecución.
En .NET Framework 2.0 se agregan nuevos miembros a la clase Type para habilitar la información de tiempo de
ejecución para tipos genéricos. Vea la documentación sobre estas clases para obtener más información sobre
cómo usar estos métodos y propiedades. El espacio de nombres System.Reflection.Emit también contiene los
miembros nuevos que admiten genéricos. Vea Cómo: Definir un tipo genérico con emisión de reflexión.
Para obtener una lista de las condiciones invariables para los términos usados en la reflexión genérica, vea los
comentarios de la propiedad IsGenericType.
Además, los miembros de la clase MethodInfo habilitan la información en tiempo de ejecución para métodos
genéricos. Para obtener una lista de las condiciones invariables para los términos usados para reflejarse en
métodos genéricos, vea los comentarios de la propiedad IsGenericMethod.
Vea también
Guía de programación de C#
Genéricos
Reflexión y tipos genéricos
Genéricos
Genéricos y atributos (Guía de programación de
C#)
16/09/2021 • 2 minutes to read
Los atributos pueden aplicarse a los tipos genéricos de la misma manera que los tipos no genéricos. Para
obtener más información sobre la aplicación de los atributos, vea Atributos.
Los atributos personalizados solo se permiten para hacer referencia a tipos genéricos abiertos, que son tipos
genéricos para los que no se proporciona ningún argumento de tipo, y tipos genéricos construidos cerrados,
que proporcionan argumentos para todos los parámetros de tipo.
En los ejemplos siguientes se usa este atributo personalizado:
[CustomAttribute(info = typeof(GenericClass1<>))]
class ClassA { }
Especifica varios parámetros de tipo con el número de comas apropiado. En este ejemplo, GenericClass2 tiene
dos parámetros de tipo:
[CustomAttribute(info = typeof(GenericClass2<,>))]
class ClassB { }
Un atributo que hace referencia a un parámetro de tipo genérico provocará un error en tiempo de compilación:
Consulte también
Guía de programación de C#
Genéricos
Atributos
Sistema de archivos y el Registro (Guía de
programación de C#)
16/09/2021 • 2 minutes to read
En los artículos siguientes se muestra cómo usar C# y .NET para realizar diversas operaciones básicas en los
archivos, las carpetas y el Registro.
En esta sección
T ÍT ULO DESC RIP C IÓ N
Procedimiento para recorrer en iteración un árbol de Muestra cómo realizar una iteración manual a través de un
directorio árbol de directorio.
Procedimiento para obtener información sobre archivos, Muestra cómo recuperar información como las horas de
carpetas y unidades creación y el tamaño, así como sobre archivos, carpetas y
unidades.
Procedimiento para crear archivos o carpetas Muestra cómo crear un archivo o una carpeta nuevos.
Procedimiento para copiar, eliminar y mover archivos y Muestra cómo copiar, eliminar y mover archivos y carpetas.
carpetas (Guía de programación de C#)
Procedimiento para proporcionar un cuadro de diálogo de Muestra cómo mostrar un cuadro de diálogo de progreso
progreso para operaciones de archivos estándar de Windows para determinadas operaciones de
archivo.
Procedimiento para escribir en un archivo texto Muestra cómo escribir en un archivo de texto.
Procedimiento para leer de un archivo de texto Muestra cómo leer de un archivo de texto.
Procedimiento para leer un archivo de texto línea a línea Muestra cómo recuperar el texto de un archivo línea a línea.
Procedimiento para crear una clave en el Registro Muestra cómo escribir una clave en el registro del sistema.
Secciones relacionadas
E/S de archivos y secuencias
Procedimiento para copiar, eliminar y mover archivos y carpetas (Guía de programación de C#)
Guía de programación de C#
System.IO
Procedimiento Recorrer en iteración un árbol de
directorio (Guía de programación de C#)
16/09/2021 • 7 minutes to read
La frase "recorrer en iteración un árbol de directorios" significa obtener acceso a cada uno de los archivos de
todos los subdirectorios anidados bajo una carpeta raíz especificada hasta un nivel de profundidad cualquiera.
No es necesario abrir cada archivo. Simplemente puede recuperar el nombre del archivo o subdirectorio como
un string , o puede recuperar información adicional en el formato de un objeto System.IO.FileInfo o
System.IO.DirectoryInfo.
NOTE
En Windows, los términos "directorio" y "carpeta" se usan indistintamente. La mayor parte de la documentación y del
texto de la interfaz de usuario usa el término "carpeta", pero las bibliotecas de clases de .NET usan el término "directorio".
En el caso más simple, en el que sabe con seguridad que tiene permisos de acceso para todos los directorios
incluidos en una raíz especificada, puede usar la marca System.IO.SearchOption.AllDirectories . Esta marca
devuelve todos los subdirectorios anidados que coinciden con el patrón especificado. En el ejemplo siguiente se
muestra cómo usar esta marca.
root.GetDirectories("*.*", System.IO.SearchOption.AllDirectories);
El punto débil de este enfoque es que si uno de los subdirectorios incluidos en la raíz especificada produce una
excepción DirectoryNotFoundException o UnauthorizedAccessException, se produce un error en todo el método
y no devuelve ningún directorio. Sucede lo mismo cuando usa el método GetFiles. Si tiene que controlar estas
excepciones en subcarpetas específicas, debe recorrer manualmente el árbol de directorios, como se muestra en
los ejemplos siguientes.
Al recorrer manualmente un árbol de directorios, puede controlar primero los archivos (recorrido en preorden)
o los subdirectorios (recorrido en postorden). Si realiza un recorrido en preorden, visitará los archivos
directamente en esa carpeta y, a continuación, recorrerá todo el árbol en la carpeta actual. El recorrido en
postorden funciona al revés, recorriendo todo el árbol por debajo antes de llegar a los archivos de la carpeta
actual. Los ejemplos que se proporcionan más adelante en este documento realizan un recorrido en preorden,
pero puede modificarlos fácilmente para que realicen un recorrido en postorden.
Otra opción consiste en usar la recursividad o un recorrido basado en la pila. Los ejemplos que se proporcionan
más adelante en este documento muestran ambos enfoques.
Si tiene que realizar diversas operaciones en los archivos y las carpetas, puede dividir estos ejemplos en partes
mediante la refactorización de la operación en funciones separadas que se puedan invocar usando un solo
delegado.
NOTE
Los sistemas de archivos NTFS pueden contener puntos de reanálisis en forma de puntos de unión, vínculos simbólicos y
vínculos físicos. Los métodos de .NET Framework como GetFiles y GetDirectories no devolverán ningún subdirectorio en
un punto de reanálisis. Este comportamiento protege frente al riesgo de provocar un bucle infinito cuando dos puntos de
reanálisis se hacen referencia entre sí. En general, debería ser muy cuidadoso al tratar con puntos de reanálisis para
asegurarse de no modificar o eliminar archivos involuntariamente. Si quiere obtener un control preciso de los puntos de
reanálisis, use la invocación de plataforma o código nativo para llamar directamente a los métodos de sistema de archivos
Win32 adecuados.
Ejemplos
En el ejemplo siguiente se muestra cómo recorrer un árbol de directorios mediante recursividad. El enfoque
recursivo resulta elegante, pero puede producir una excepción de desbordamiento de la pila si el árbol de
directorios es grande y cuenta con muchos elementos anidados.
Las excepciones concretas que se controlan y las acciones determinadas que se realizan en cada archivo o
carpeta se proporcionan simplemente como ejemplos. Debe modificar este código para que se ajuste a sus
requisitos concretos. Para obtener más información, vea los comentarios del código.
catch (System.IO.DirectoryNotFoundException e)
{
Console.WriteLine(e.Message);
}
if (files != null)
{
foreach (System.IO.FileInfo fi in files)
{
// In this example, we only access the existing FileInfo object. If we
// want to open, delete or modify the file, then
// a try-catch block is required here to handle the case
// where the file has been deleted since the call to TraverseTree().
Console.WriteLine(fi.FullName);
}
En el ejemplo siguiente se muestra cómo recorrer en iteración los archivos y las carpetas de un árbol de
directorios sin usar la recursividad. Esta técnica usa el tipo de colección genérica Stack<T>, que es una pila de
tipo LIFO (último en entrar, primero en salir).
Las excepciones concretas que se controlan y las acciones determinadas que se realizan en cada archivo o
carpeta se proporcionan simplemente como ejemplos. Debe modificar este código para que se ajuste a sus
requisitos concretos. Para obtener más información, vea los comentarios del código.
if (!System.IO.Directory.Exists(root))
{
throw new ArgumentException();
}
dirs.Push(root);
catch (UnauthorizedAccessException e)
{
Console.WriteLine(e.Message);
continue;
}
catch (System.IO.DirectoryNotFoundException e)
{
Console.WriteLine(e.Message);
continue;
}
// Perform the required action on each file here.
// Modify this block to perform your required task.
foreach (string file in files)
{
try
{
// Perform whatever action is required in your scenario.
System.IO.FileInfo fi = new System.IO.FileInfo(file);
Console.WriteLine("{0}: {1}, {2}", fi.Name, fi.Length, fi.CreationTime);
}
catch (System.IO.FileNotFoundException e)
{
// If file was deleted by a separate application
// or thread since the call to TraverseTree()
// then just continue.
Console.WriteLine(e.Message);
continue;
}
}
Generalmente se tarda mucho tiempo en comprobar cada carpeta para determinar si su aplicación tiene
permiso para abrirla. Por consiguiente, el ejemplo de código incluye esa parte de la operación en un bloque
try/catch . Puede modificar el bloque catch de manera que, cuando se le deniegue el acceso a una carpeta,
intente elevar sus permisos y obtener acceso a esta de nuevo. Como norma, detecte solamente las excepciones
que puede controlar sin dejar la aplicación en un estado desconocido.
Si debe almacenar el contenido de un árbol de directorios, ya sea en memoria o en el disco, la mejor opción es
almacenar solamente la propiedad FullName (de tipo string ) para cada archivo. Después, puede usar esta
cadena para crear un nuevo objeto FileInfo o DirectoryInfo, según sea necesario, o para abrir cualquier archivo
que requiera un procesamiento adicional.
Programación sólida
Un código eficaz de iteración de archivos debe tener en cuenta las numerosas dificultades del sistema de
archivos. Para más información sobre el sistema de archivos de Windows, vea NTFS overview (Introducción a
NTFS).
Vea también
System.IO
LINQ y directorios de archivos
Registro y sistema de archivos (Guía de programación de C#)
Procedimiento Obtener información sobre archivos,
carpetas y unidades (Guía de programación de C#)
16/09/2021 • 2 minutes to read
En .NET, puede acceder a información del sistema de archivos mediante las clases siguientes:
System.IO.FileInfo
System.IO.DirectoryInfo
System.IO.DriveInfo
System.IO.Directory
System.IO.File
Las clases FileInfo y DirectoryInfo representan un archivo o directorio y contienen propiedades que exponen
muchos de los atributos de archivo admitidos por el sistema de archivos NTFS. También contienen métodos
para abrir, cerrar, mover y eliminar archivos y carpetas. Para crear instancias de estas clases, pase una cadena
que represente el nombre del archivo, carpeta o unidad en el constructor:
También puede obtener los nombres de archivos, carpetas o unidades mediante llamadas a
DirectoryInfo.GetDirectories, DirectoryInfo.GetFiles y DriveInfo.RootDirectory.
Las clases System.IO.Directory y System.IO.File proporcionan métodos estáticos para recuperar información
sobre directorios y archivos.
Ejemplo
En el ejemplo siguiente se muestran diversas maneras de obtener acceso a información sobre archivos y
carpetas.
class FileSysInfo
{
static void Main()
{
// You can also use System.Environment.GetLogicalDrives to
// obtain names of all logical drives on the computer.
System.IO.DriveInfo di = new System.IO.DriveInfo(@"C:\");
Console.WriteLine(di.TotalFreeSpace);
Console.WriteLine(di.VolumeLabel);
// Get the root directory and print out some information about it.
System.IO.DirectoryInfo dirInfo = di.RootDirectory;
Console.WriteLine(dirInfo.Attributes.ToString());
// Get the files in the directory and print out some information about them.
System.IO.FileInfo[] fileNames = dirInfo.GetFiles("*.*");
System.IO.Directory.SetCurrentDirectory(@"C:\Users\Public\TestFolder\");
currentDirName = System.IO.Directory.GetCurrentDirectory();
Console.WriteLine(currentDirName);
Programación sólida
Al procesar cadenas de ruta de acceso especificadas por el usuario, también debe controlar las excepciones para
las condiciones siguientes:
El nombre del archivo es incorrecto. Por ejemplo, contiene caracteres no válidos o solo espacios en
blanco.
El nombre del archivo es nulo.
La longitud del nombre de archivo es superior a la longitud máxima definida por el sistema.
El nombre de archivo contiene un signo de dos puntos (:).
Si la aplicación no tiene permisos suficientes para leer el archivo especificado, el método Exists devuelve
false con independencia de si existe una ruta de acceso; el método no produce una excepción.
Vea también
System.IO
Guía de programación de C#
Registro y sistema de archivos (Guía de programación de C#)
Procedimiento Crear archivos o carpetas (Guía de
programación de C#)
16/09/2021 • 3 minutes to read
Puede crear una carpeta en el equipo mediante programación, crear una subcarpeta, crear un archivo en la
subcarpeta y escribir datos en el archivo.
Ejemplo
public class CreateFileOrFolder
{
static void Main()
{
// Specify a name for your top-level folder.
string folderName = @"c:\Top-Level Folder";
// You can write out the path name directly instead of using the Combine
// method. Combine just makes the process easier.
string pathString2 = @"c:\Top-Level Folder\SubFolder2";
// You can extend the depth of your path if you want to.
//pathString = System.IO.Path.Combine(pathString, "SubSubFolder");
// Create the subfolder. You can verify in File Explorer that you have this
// structure in the C: drive.
// Local Disk (C:)
// Top-Level Folder
// SubFolder
System.IO.Directory.CreateDirectory(pathString);
// This example uses a random string for the name, but you also can specify
// a particular name.
//string fileName = "MyNewFile.txt";
// Check that the file doesn't already exist. If it doesn't exist, create
// the file and write integers 0 - 99 to it.
// DANGER: System.IO.File.Create will overwrite the file if it already exists.
// This could happen even with random file names, although it is unlikely.
if (!System.IO.File.Exists(pathString))
{
using (System.IO.FileStream fs = System.IO.File.Create(pathString))
{
for (byte i = 0; i < 100; i++)
{
fs.WriteByte(i);
}
}
}
}
else
{
Console.WriteLine("File \"{0}\" already exists.", fileName);
return;
}
//0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
//30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
// 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 8
//3 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
}
Si la carpeta ya existe, CreateDirectory no efectúa ninguna acción y no se devuelve ninguna excepción. Sin
embargo, File.Create reemplaza un archivo existente por otro nuevo. En el ejemplo se usa una instrucción if -
else para evitar que se sustituya un archivo existente.
Al realizar los siguientes cambios en el ejemplo, puede especificar diferentes resultados en función de si ya
existe un archivo con un nombre determinado. Si no existe ese archivo, el código crea uno. Si ese archivo ya
existe, el código anexa datos a ese archivo.
Especifique un nombre de archivo no aleatorio.
Seguridad de .NET
En los casos de confiabilidad parcial, es posible que se devuelva una instancia de la clase SecurityException.
Si no tiene permiso para crear la carpeta, el ejemplo devuelve una instancia de la clase
UnauthorizedAccessException.
Vea también
System.IO
Guía de programación de C#
Registro y sistema de archivos (Guía de programación de C#)
Procedimiento Copiar, eliminar y mover archivos y
carpetas (Guía de programación de C#)
16/09/2021 • 3 minutes to read
En los siguientes ejemplos se muestra cómo copiar, mover y eliminar archivos y carpetas de una manera
sincrónica con las clases System.IO.File, System.IO.Directory, System.IO.FileInfo y System.IO.DirectoryInfo desde
el espacio de nombres System.IO. En estos ejemplos no se proporciona una barra de progreso ni ninguna otra
interfaz de usuario. Si quiere proporcionar un cuadro de diálogo de progreso estándar, consulte Procedimiento
Proporcionar un cuadro de diálogo de progreso para operaciones de archivos.
Use System.IO.FileSystemWatcher para proporcionar eventos que le permitan calcular el progreso al realizar
operaciones en varios archivos. Otro enfoque consiste en usar la invocación de plataforma para llamar a los
métodos pertinentes relacionados con archivos en el shell de Windows. Para obtener información sobre cómo
realizar estas operaciones de archivo de forma asincrónica, vea E/S de archivos asincrónica.
Ejemplos
En el ejemplo siguiente se muestra cómo copiar archivos y directorios.
// Simple synchronous file copy operations with no user interface.
// To run this sample, first create the following directories and files:
// C:\Users\Public\TestFolder
// C:\Users\Public\TestFolder\test.txt
// C:\Users\Public\TestFolder\SubDir\test.txt
public class SimpleFileCopy
{
static void Main()
{
string fileName = "test.txt";
string sourcePath = @"C:\Users\Public\TestFolder";
string targetPath = @"C:\Users\Public\TestFolder\SubDir";
// Copy the files and overwrite destination files if they already exist.
foreach (string s in files)
{
// Use static Path methods to extract only the file name from the path.
fileName = System.IO.Path.GetFileName(s);
destFile = System.IO.Path.Combine(targetPath, fileName);
System.IO.File.Copy(s, destFile, true);
}
}
else
{
Console.WriteLine("Source path does not exist!");
}
catch (System.IO.IOException e)
{
Console.WriteLine(e.Message);
}
}
Consulte también
System.IO
Guía de programación de C#
Registro y sistema de archivos (Guía de programación de C#)
Procedimiento para proporcionar un cuadro de diálogo de progreso para operaciones de archivos
E/S de archivos y secuencias
Tareas de E/S comunes
Procedimiento Proporcionar un cuadro de diálogo
de progreso para operaciones de archivos (Guía de
programación de C#)
16/09/2021 • 2 minutes to read
Puede proporcionar un cuadro de diálogo estándar que muestra el progreso en las operaciones de archivo en
Windows si usa el método CopyFile(String, String, UIOption) en el espacio de nombres Microsoft.VisualBasic.
NOTE
Es posible que el equipo muestre nombres o ubicaciones diferentes para algunos de los elementos de la interfaz de
usuario de Visual Studio en las siguientes instrucciones. La edición de Visual Studio que se tenga y la configuración que se
utilice determinan estos elementos. Para obtener más información, vea Personalizar el IDE.
Ejemplo
El siguiente código copia el directorio que especifica sourcePath en el directorio que especifica destinationPath
. Este código también proporciona un cuadro de diálogo estándar en el que se muestra el tiempo estimado
restante antes de que finalice la operación.
class FileProgress
{
static void Main()
{
// Specify the path to a folder that you want to copy. If the folder is small,
// you won't have time to see the progress dialog box.
string sourcePath = @"C:\Windows\symbols\";
// Choose a destination for the copied files.
string destinationPath = @"C:\TestFolder";
FileSystem.CopyDirectory(sourcePath, destinationPath,
UIOption.AllDialogs);
}
}
Consulte también
Registro y sistema de archivos (Guía de programación de C#)
Procedimiento Escribir en un archivo de texto (Guía
de programación de C#)
16/09/2021 • 3 minutes to read
En este artículo hay varios ejemplos en los que muestran distintas formas de escribir texto en un archivo. En los
dos primeros se usan métodos estáticos útiles de la clase System.IO.File para escribir cada elemento de
cualquier interfaz IEnumerable<string> y un elemento string en un archivo de texto. En el tercer ejemplo se
muestra cómo agregar texto a un archivo cuando hay que procesar cada línea individualmente a medida que se
escribe en el archivo. En los tres primeros ejemplos se sobrescribe todo el contenido existente del archivo. En el
último ejemplo se muestra cómo anexar texto a un archivo existente.
Todos estos ejemplos escriben literales de cadena en los archivos. Si quiere aplicar formato al texto escrito en un
archivo, use el método Format o la característica interpolación de cadenas de C#.
class WriteAllLines
{
public static async Task ExampleAsync()
{
string[] lines =
{
"First line", "Second line", "Third line"
};
class WriteAllText
{
public static async Task ExampleAsync()
{
string text =
"A class is the most powerful data type in C#. Like a structure, " +
"a class defines the data and behavior of the data type. ";
class StreamWriterOne
{
public static async Task ExampleAsync()
{
string[] lines = { "First line", "Second line", "Third line" };
using StreamWriter file = new("WriteLines2.txt");
class StreamWriterTwo
{
public static async Task ExampleAsync()
{
using StreamWriter file = new("WriteLines2.txt", append: true);
await file.WriteLineAsync("Fourth line");
}
}
Excepciones
Las condiciones siguientes pueden provocar una excepción:
InvalidOperationException; El archivo ya existe y es de solo lectura.
PathTooLongException; Puede que el nombre de ruta de acceso sea demasiado largo.
IOException; Es posible que el disco esté lleno.
Existen condiciones adicionales que pueden iniciar excepciones cuando se trabaja con el sistema de archivos; es
mejor programar de forma defensiva.
Vea también
Guía de programación de C#
Registro y sistema de archivos (Guía de programación de C#)
Ejemplo: guardar una colección en el almacenamiento para la aplicación
Procedimiento Leer de un archivo de texto (Guía de
programación de C#)
16/09/2021 • 2 minutes to read
En este ejemplo se lee el contenido de un archivo de texto con los métodos estáticos ReadAllText y ReadAllLines
de la clase System.IO.File.
Para obtener un ejemplo en el que se usa StreamReader, consulte Procedimiento Leer un archivo de texto línea a
línea.
NOTE
Los archivos que se usan en este ejemplo se crean en el tema Procedimiento Escribir en un archivo texto.
Ejemplo
class ReadFromFile
{
static void Main()
{
// The files used in this example are created in the topic
// How to: Write to a Text File. You can change the path and
// file name to substitute text files of your own.
// Example #1
// Read the file as one string.
string text = System.IO.File.ReadAllText(@"C:\Users\Public\TestFolder\WriteText.txt");
// Example #2
// Read each line of the file into a string array. Each element
// of the array is one line of the file.
string[] lines = System.IO.File.ReadAllLines(@"C:\Users\Public\TestFolder\WriteLines2.txt");
Compilar el código
Copie el código y péguelo en una aplicación de consola de C#.
Si no está usando los archivos de texto de Procedimiento Escribir en un archivo texto, reemplace el argumento a
ReadAllText ya ReadAllLines por la ruta de acceso adecuada y el nombre de archivo en el equipo.
Programación sólida
Las condiciones siguientes pueden provocar una excepción:
El archivo no existe o no existe en la ubicación especificada. Compruebe la ruta de acceso y la ortografía del
nombre de archivo.
Seguridad de .NET
No confíe en el nombre de un archivo para determinar el contenido del archivo. Por ejemplo, el archivo
myFile.cs puede que no sea un archivo de código fuente de C#.
Vea también
System.IO
Guía de programación de C#
Registro y sistema de archivos (Guía de programación de C#)
Procedimiento para leer un archivo de texto de
línea en línea (guía de programación de C#)
16/09/2021 • 2 minutes to read
En este ejemplo se lee el contenido de un archivo de texto línea a línea en una cadena mediante el método
ReadLine de la clase StreamReader . Cada línea de texto se almacena en la cadena line y se muestra en la
pantalla.
Ejemplo
int counter = 0;
string line;
file.Close();
System.Console.WriteLine("There were {0} lines.", counter);
// Suspend the screen.
System.Console.ReadLine();
Compilar el código
Copie el código y péguelo en el método Main de una aplicación de consola.
Reemplace "c:\test.txt" por el nombre de archivo real.
Programación sólida
Las condiciones siguientes pueden provocar una excepción:
El archivo podría no existir.
Seguridad de .NET
No tome ninguna decisión sobre el contenido del archivo basándose en su nombre. Por ejemplo, es posible que
el archivo myFile.cs no sea un archivo de código fuente de C#.
Vea también
System.IO
Guía de programación de C#
Registro y sistema de archivos (Guía de programación de C#)
Procedimiento para crear una clave del Registro
(guía de programación de C#)
16/09/2021 • 2 minutes to read
En este ejemplo se agrega el par de valores "Name" e "Isabella" al Registro del usuario actual en la clave
"Names".
Ejemplo
Microsoft.Win32.RegistryKey key;
key = Microsoft.Win32.Registry.CurrentUser.CreateSubKey("Names");
key.SetValue("Name", "Isabella");
key.Close();
Compilar el código
Copie el código y péguelo en el método Main de una aplicación de consola.
Sustituya el parámetro Names por el nombre de una clave que exista directamente en el nodo
HKEY_CURRENT_USER del Registro.
Sustituya el parámetro Name por el nombre de un valor que exista directamente en el nodo Names.
Programación sólida
Examine la estructura del Registro para buscar una ubicación adecuada para la clave. Por ejemplo, es posible que
quiera abrir la clave Software del usuario actual y crear una clave con el nombre de la empresa. Luego agregue
los valores del Registro a la clave de la empresa.
Las condiciones siguientes pueden generar una excepción:
Que el nombre de la clave sea nulo.
Que el usuario no tenga permisos para crear claves del Registro.
Que el nombre de la clave supere el límite de 255 caracteres.
Que la clave esté cerrada.
Que la clave del Registro sea de solo lectura.
Seguridad de .NET
Es más seguro escribir datos en la carpeta de usuario ( Microsoft.Win32.Registry.CurrentUser ) que en el equipo
local ( Microsoft.Win32.Registry.LocalMachine ).
Cuando se crea un valor del Registro, se debe decidir qué hacer si ese valor ya existe. Puede que otro proceso,
quizás uno malintencionado, ya haya creado el valor y tenga acceso a él. Al colocar datos en el valor del
Registro, estos están a disposición del otro proceso. Para evitarlo, use el método
Overload:Microsoft.Win32.RegistryKey.GetValue . Si la clave aún no existe, devuelve null.
No es seguro almacenar secretos, como contraseñas, en el Registro como texto sin formato, aunque la clave del
Registro esté protegida mediante listas de control de acceso (ACL).
Vea también
System.IO
Guía de programación de C#
Registro y sistema de archivos (Guía de programación de C#)
Read, write and delete from the registry with C# (Leer, escribir y eliminar en el Registro con C#)
Interoperabilidad (Guía de programación de C#)
16/09/2021 • 2 minutes to read
En esta sección
Información general sobre interoperabilidad
Se describen métodos para habilitar la interoperabilidad entre el código administrado y el código no
administrado de C#.
Procedimiento para acceder a objetos de interoperabilidad de Office mediante características de C#
Describe las características introducidas en Visual C# para facilitar la programación de Office.
Procedimiento para usar propiedades indizadas en la programación de interoperabilidad COM
Se describe cómo utilizar las propiedades indizadas para acceder a las propiedades de COM que tienen
parámetros.
Procedimiento para usar la invocación de plataforma para reproducir un archivo WAV
Se describe cómo usar los servicios de invocación de plataforma para reproducir un archivo de sonido .wav en
el sistema operativo Windows.
Tutorial: Programación de Office
Muestra cómo crear un libro de Excel y un documento de Word que contiene un vínculo al libro.
Clase COM de ejemplo
Muestra cómo exponer una clase de C# como un objeto COM.
Vea también
Marshal.ReleaseComObject
Guía de programación de C#
Interoperar con código no administrado
Tutorial: Programación de Office
Información general sobre interoperabilidad (Guía
de programación de C#)
16/09/2021 • 3 minutes to read
En el tema se describen métodos para habilitar la interoperabilidad entre el código administrado y el código no
administrado de C#.
Invocación de plataforma
La invocación de plataforma es un servicio que permite al código administrado llamar a funciones no
administradas que se implementan en bibliotecas de vínculos dinámicos (DLL), como las de la API de Microsoft
Windows. Busca y llama a una función exportada y calcula las referencias de sus argumentos (enteros, cadenas,
matrices, estructuras etc.) a través de los límites de interoperación según sea necesario.
Para más información, consulte Consumir funciones DLL no administradas y Procedimiento Utilizar la
invocación de plataforma para reproducir un archivo de sonido.
NOTE
Common Language Runtime (CLR) administra el acceso a los recursos del sistema. Si se llama al código no administrado
que está fuera de CLR, se omite este mecanismo de seguridad y, por lo tanto, existe un riesgo de seguridad. Por ejemplo,
el código no administrado podría llamar directamente a recursos en código no administrado, omitiendo los mecanismos
de seguridad de CLR. Para obtener más información, vea Seguridad en .NET.
Interoperabilidad de C++
Puede usar la interoperabilidad de C++, también conocida como It Just Works (IJW), para encapsular una clase
de C++ nativa de modo que el código creado en C# o en otro lenguaje de .NET pueda consumirla. Para ello,
escriba código de C++ para encapsular un componente DLL o COM nativo. A diferencia de otros lenguajes de
.NET, Visual C++ cuenta con compatibilidad de interoperabilidad que permite que haya código administrado y
no administrado en la misma aplicación, e incluso en el mismo archivo. Después, compile el código de C++
mediante el modificador del compilador /clr para generar un ensamblado administrado. Finalmente, agregue
una referencia al ensamblado en el proyecto de C# y use los objetos encapsulados igual que usaría otras clases
administradas.
Exponer C# en COM
Los clientes COM pueden consumir tipos de C# que se han expuesto correctamente. Los pasos básicos para
exponer tipos de C# son los siguientes:
1. Agregue atributos de interoperabilidad al proyecto de C#.
Puede hacer que un ensamblado COM sea visible al modificar las propiedades del proyecto de Visual C#.
Para obtener más información, vea Información de ensamblado (Cuadro de diálogo).
2. Genere una biblioteca de tipos COM y regístrela para el uso de COM.
Puede modificar las propiedades del proyecto de Visual C# para registrar automáticamente el
ensamblado de C# para la interoperabilidad COM. Visual Studio usa Regasm.exe (herramienta de registro
de ensamblados), con el modificador de la línea de comandos /tlb , que toma un ensamblado
administrado como entrada, para generar una biblioteca de tipos. Esta biblioteca de tipos describe los
tipos public del ensamblado y agrega entradas del registro para que los clientes COM puedan crear
clases administradas.
Para obtener más información, vea Exponer componentes de .NET Framework en COM y Clase COM de ejemplo.
Vea también
Improving Interop Performance (Mejorar el rendimiento interoperativo)
Introducción a la interoperabilidad entre COM y .NET
Información general sobre la interoperabilidad COM (Visual Basic)
Serialización de interoperabilidad
Interoperating with Unmanaged Code (Interoperar con código no administrado)
Guía de programación de C#
Procedimiento Tener acceso a objetos de
interoperabilidad de Office mediante las
características de Visual C# (Guía de programación
de C#)
16/09/2021 • 12 minutes to read
C# tiene nuevas características que simplifican el acceso a objetos de la API de Office. Las nuevas características
incluyen argumentos con nombre y opcionales, un nuevo tipo llamado dynamic y la capacidad de pasar
argumentos a parámetros de referencia en los métodos COM como si fueran parámetros de valor.
En este tema se utilizarán las nuevas características para escribir código que crea y muestra una hoja de cálculo
de Microsoft Office Excel. A continuación, se escribirá código para agregar un documento de Office Word que
contiene un icono que está vinculado a la hoja de cálculo de Excel.
Para completar este tutorial, es necesario tener Microsoft Office Excel 2007 y Microsoft Office Word 2007 —o
una versión posterior— instalados en el equipo.
NOTE
Es posible que el equipo muestre nombres o ubicaciones diferentes para algunos de los elementos de la interfaz de
usuario de Visual Studio en las siguientes instrucciones. La edición de Visual Studio que se tenga y la configuración que se
utilice determinan estos elementos. Para obtener más información, vea Personalizar el IDE.
2. Agregue el código siguiente al método Main para crear una lista bankAccounts lista que contenga dos
cuentas.
2. Agregue el siguiente código al final de DisplayInExcel . El código inserta valores en las dos primeras
columnas de la primera fila de la hoja de cálculo.
3. Agregue el siguiente código al final de DisplayInExcel . El bucle foreach coloca la información de la lista
de cuentas en las dos primeras columnas de filas sucesivas de la hoja de cálculo.
var row = 1;
foreach (var acct in accounts)
{
row++;
workSheet.Cells[row, "A"] = acct.ID;
workSheet.Cells[row, "B"] = acct.Balance;
}
4. Agregue el código siguiente al final de DisplayInExcel para ajustar los anchos de columna a fin de
adaptarlos al contenido.
workSheet.Columns[1].AutoFit();
workSheet.Columns[2].AutoFit();
Las versiones anteriores de C# requieren una conversión explícita para estas operaciones, ya que
ExcelApp.Columns[1] devuelve Object y AutoFit es un método Range de Excel. Las siguientes líneas
muestran la conversión.
((Excel.Range)workSheet.Columns[1]).AutoFit();
((Excel.Range)workSheet.Columns[2]).AutoFit();
2. Presione CTRL+F5.
Aparece una hoja de cálculo de Excel que contiene los datos de las dos cuentas.
// The Add method has four reference parameters, all of which are
// optional. Visual C# allows you to omit arguments for them if
// the default values are what you want.
wordApp.Documents.Add();
En C# 3.0 y versiones anteriores del lenguaje, es necesario el código siguiente, que es más complejo.
// The Add method has four parameters, all of which are optional.
// In Visual C# 2008 and earlier versions, an argument has to be sent
// for every parameter. Because the parameters are reference
// parameters of type object, you have to create an object variable
// for the arguments that represents 'no value'.
3. Agregue la siguiente instrucción al final de DisplayInExcel . El método Copy agrega la hoja de cálculo en
el Portapapeles.
// Put the spreadsheet contents on the clipboard. The Copy method has one
// optional parameter for specifying a destination. Because no argument
// is sent, the destination is the Clipboard.
workSheet.Range["A1:B3"].Copy();
4. Presione CTRL+F5.
Un documento de Word aparecerá con un icono. Haga doble clic en el icono para abrir la hoja de cálculo
en primer plano.
((Excel.Range)workSheet.Columns[1]).AutoFit();
((Excel.Range)workSheet.Columns[2]).AutoFit();
2. Para cambiar el valor predeterminado y usar los PIA en lugar de insertar información de tipos, expanda el
nodo Referencias del Explorador de soluciones y, después, seleccione
Microsoft.Office.Interop.Excel o Microsoft.Office.Interop.Word .
3. Si no ve la ventana Propiedades , presione F4 .
4. Busque Incrustar tipos de interoperabilidad en la lista de propiedades y cambie su valor a False . Del
mismo modo, se puede compilar mediante la opción del compilador References en lugar de
EmbedInteropTypes en un símbolo del sistema.
El método AutoFormat tiene siete parámetros de valor, todos ellos opcionales. Los argumentos con
nombre y los argumentos opcionales permiten proporcionar argumentos para ninguno, algunos o todos
ellos. En la instrucción anterior, se proporciona un argumento para uno solo de los parámetros, Format .
Puesto que Format es el primer parámetro de la lista de parámetros, no es necesario proporcionar el
nombre de parámetro. Sin embargo, la instrucción sería más fácil de entender si se incluyese el nombre
del parámetro, como se muestra en el código siguiente.
Ejemplo
En el código siguiente se muestra el ejemplo completo.
using System;
using System.Collections.Generic;
using System.Linq;
using Excel = Microsoft.Office.Interop.Excel;
using Word = Microsoft.Office.Interop.Word;
namespace OfficeProgramminWalkthruComplete
{
class Walkthrough
{
static void Main(string[] args)
{
// Create a list of accounts.
var bankAccounts = new List<Account>
{
new Account {
ID = 345678,
Balance = 541.27
},
new Account {
ID = 1230221,
Balance = -127.44
}
};
var row = 1;
foreach (var acct in accounts)
{
row++;
workSheet.Cells[row, "A"] = acct.ID;
workSheet.Cells[row, "B"] = acct.Balance;
}
workSheet.Columns[1].AutoFit();
workSheet.Columns[2].AutoFit();
// Put the spreadsheet contents on the clipboard. The Copy method has one
// optional parameter for specifying a destination. Because no argument
// is sent, the destination is the Clipboard.
workSheet.Range["A1:B3"].Copy();
}
// The Add method has four reference parameters, all of which are
// optional. Visual C# allows you to omit arguments for them if
// the default values are what you want.
wordApp.Documents.Add();
Consulte también
Type.Missing
dynamic
Uso de tipo dinámico
Argumentos opcionales y con nombre
Procedimiento para usar argumentos opcionales y con nombre en la programación de Office
Procedimiento Utilizar propiedades indizadas en la
programación de interoperabilidad COM (Guía de
programación de C#)
16/09/2021 • 2 minutes to read
Las propiedades indexadas mejoran la manera en que se usan las propiedades COM con parámetros en la
programación de C#. Las propiedades indexadas funcionan junto con otras características de Visual C#, como
los argumentos con nombre y opcionales, un nuevo tipo (dinámico) y la información de tipo insertada, para
mejorar la programación en Microsoft Office.
En versiones anteriores de C#, los métodos son solo accesibles como propiedades si el método get no tiene
ningún parámetro y el método set tiene solo un parámetro de valor. En cambio, no todas las propiedades
COM cumplen esas restricciones. Por ejemplo, la propiedad Range[] de Excel tiene un descriptor de acceso get
que requiere un parámetro para el nombre del intervalo. Antes, como no había acceso directo a la propiedad
Range , había que usar el método get_Range en su lugar, como se muestra en el siguiente ejemplo.
// Visual C# 2010.
var excelApp = new Excel.Application();
// . . .
Excel.Range targetRange = excelApp.Range["A1"];
NOTE
En el ejemplo anterior, también se usa la característica argumentos opcionales, que le permite omitir Type.Missing .
De manera similar, para establecer el valor de la propiedad Value de un objeto Range en C# 3.0 y versiones
anteriores, se necesitan dos argumentos. Uno proporciona un argumento para un parámetro opcional que
especifica el tipo del valor del intervalo. El otro proporciona el valor de la propiedad Value . Estas técnicas se
ilustran en los siguientes ejemplos. En ambos ejemplos, se establece el valor de la celda A1 en Name .
// Visual C# 2008.
targetRange.set_Value(Type.Missing, "Name");
// Or
targetRange.Value2 = "Name";
// Visual C# 2010.
targetRange.Value = "Name";
No es posible crear propiedades indizadas propias. La característica solo admite el uso de las propiedades
indizadas existentes.
Ejemplo
En el código siguiente se muestra un ejemplo completo. Para más información sobre cómo preparar un
proyecto con acceso a la API de Office, consulte Procedimiento Tener acceso a objetos de interoperabilidad de
Office mediante las características de Visual C# (Guía de programación de C#).
namespace IndexedProperties
{
class Program
{
static void Main(string[] args)
{
CSharp2010();
//CSharp2008();
}
Vea también
Argumentos opcionales y con nombre
dynamic
Uso de tipo dinámico
Procedimiento para usar argumentos opcionales y con nombre en la programación de Office
Procedimiento para acceder a objetos de interoperabilidad de Office mediante características de C#
Tutorial: Programación de Office
Procedimiento Utilizar la invocación de plataforma
para reproducir un archivo de sonido (Guía de
programación de C#)
16/09/2021 • 2 minutes to read
En el siguiente ejemplo de código de C# se muestra cómo se usan los servicios de invocación de plataforma
para reproducir un archivo de sonido WAV en el sistema operativo Windows.
Ejemplo
En este código de ejemplo se usa DllImportAttribute para importar el punto de entrada del método winmm.dll
de PlaySound como Form1 PlaySound() . El ejemplo tiene un formulario Windows Forms simple con un botón. Al
hacer clic en el botón, se abre un cuadro de diálogo OpenFileDialog estándar de Windows para que pueda abrir
un archivo y reproducirlo. Cuando se selecciona un archivo de sonido, se reproduce mediante el método
PlaySound() de la biblioteca winmm.dll. Para obtener más información sobre este método, vea Using the
PlaySound function with Waveform-Audio Files (Uso de la función PlaySound con archivos para forma de onda
de sonido). Busque y seleccione un archivo que tenga una extensión .wav y, después, haga clic en Abrir para
reproducirlo mediante la invocación de plataforma. Un cuadro de texto muestra la ruta de acceso completa del
archivo seleccionado.
El cuadro de diálogo Abrir archivos se puede filtrar con los siguientes valores para que muestre solo los
archivos que tengan la extensión .wav:
namespace WinSound
{
public partial class Form1 : Form
{
private TextBox textBox1;
private Button button1;
[System.Flags]
public enum PlaySoundFlags : int
{
SND_SYNC = 0x0000,
SND_ASYNC = 0x0001,
SND_NODEFAULT = 0x0002,
SND_LOOP = 0x0008,
SND_NOSTOP = 0x0010,
SND_NOWAIT = 0x00002000,
SND_FILENAME = 0x00020000,
SND_RESOURCE = 0x00040004
}
if (dialog1.ShowDialog() == DialogResult.OK)
{
textBox1.Text = dialog1.FileName;
PlaySound(dialog1.FileName, new System.IntPtr(), PlaySoundFlags.SND_SYNC);
}
}
Consulte también
Guía de programación de C#
Información general sobre interoperabilidad
Aproximación a la invocación de plataforma
Serialización de datos con invocación de plataforma
Tutorial: Programación de Office (C# y Visual Basic)
16/09/2021 • 12 minutes to read
Visual Studio presenta características en C# y Visual Basic que mejoran la programación de Microsoft Office. Las
características útiles de C# incluyen argumentos opcionales y con nombre, y devuelven valores de tipo dynamic .
En la programación COM, puede omitir la palabra clave ref y obtener acceso a las propiedades indexadas. Las
nuevas características de Visual Basic incluyen propiedades implementadas automáticamente, instrucciones de
expresiones lambda e inicializadores de colección.
En ambos lenguajes se puede insertar información de tipo, lo que permite la implementación de ensamblados
que interactúan con componentes COM sin necesidad de implementar ensamblados de interoperabilidad
primarios (PIA) en el equipo del usuario. Para obtener más información, vea Tutorial: Insertar los tipos de los
ensamblados administrados.
En este tutorial se muestran estas características en el contexto de la programación de Office, pero muchas de
ellas también son útiles en la programación general. En el tutorial, usa una aplicación complemento de Excel
para crear un libro de Excel. Después, crea un documento de Word que contiene un vínculo al libro. Por último,
ve cómo habilitar y deshabilitar la dependencia de un PIA.
Requisitos previos
Debe tener Microsoft Office Excel y Microsoft Office Word instalados en su equipo para completar este tutorial.
NOTE
Es posible que el equipo muestre nombres o ubicaciones diferentes para algunos de los elementos de la interfaz de
usuario de Visual Studio en las siguientes instrucciones. La edición de Visual Studio que se tenga y la configuración que se
utilice determinan estos elementos. Para obtener más información, vea Personalizar el IDE.
using System.Collections.Generic;
using Excel = Microsoft.Office.Interop.Excel;
using Word = Microsoft.Office.Interop.Word;
Imports Microsoft.Office.Interop
class Account
{
public int ID { get; set; }
public double Balance { get; set; }
}
3. Para crear una lista que contenga dos cuentas, agregue el código siguiente al método
bankAccounts
ThisAddIn_Startup en ThisAddIn.vb o ThisAddIn.cs. Las declaraciones de lista usan inicializadores de
colección. Para obtener más información, vea Inicializadores de colección.
var bankAccounts = new List<Account>
{
new Account
{
ID = 345,
Balance = 541.27
},
new Account
{
ID = 123,
Balance = -127.44
}
};
With Me.Application
' Add a new Excel workbook.
.Workbooks.Add()
.Visible = True
.Range("A1").Value = "ID"
.Range("B1").Value = "Balance"
.Range("A2").Select()
En este método se utilizan dos características de C# nuevas. Ambas características ya existen en Visual
Basic.
El método Add tiene un parámetro opcional para especificar una plantilla determinada. Los
parámetros opcionales introducidos en C# 4 permiten omitir el argumento para ese parámetro si
se desea utilizar el valor predeterminado del parámetro. Dado que en el ejemplo anterior no se
envía ningún argumento, Add usa la plantilla predeterminada y crea un libro nuevo. La
instrucción equivalente en versiones anteriores de C# requiere un argumento de marcador de
posición: excelApp.Workbooks.Add(Type.Missing) .
Para obtener más información, vea Argumentos opcionales y con nombre.
Las propiedades Range y Offset del objeto Range usan la característica de propiedades
indizadas. Esta característica permite utilizar estas propiedades de los tipos COM mediante la
siguiente sintaxis típica de C#. Las propiedades indizadas también permiten utilizar la propiedad
Value del objeto Range , eliminando la necesidad de utilizar la propiedad Value2 . La propiedad
Value está indizada, pero el índice es opcional. Los argumentos opcionales y las propiedades
indizadas funcionan conjuntamente en el ejemplo siguiente.
// In Visual C# 2008, you cannot access the Range, Offset, and Value
// properties directly.
excelApp.get_Range("A1").Value2 = "ID";
excelApp.ActiveCell.get_Offset(1, 0).Select();
No es posible crear propiedades indizadas propias. La característica solo admite el uso de las
propiedades indizadas existentes.
Para más información, consulte Procedimiento para usar propiedades indizadas en la
programación de interoperabilidad COM.
2. Agregue el código siguiente al final de DisplayInExcel para ajustar los anchos de columna a fin de
adaptarlos al contenido.
excelApp.Columns[1].AutoFit();
excelApp.Columns[2].AutoFit();
' Add the following two lines at the end of the With statement.
.Columns(1).AutoFit()
.Columns(2).AutoFit()
Estas adiciones muestran otra característica de C#: el tratamiento de valores Object devueltos por hosts
COM, como Office, como si tuvieran un tipo dynamic. Esto sucede automáticamente cuando Incrustar
tipos de interoperabilidad se establece en su valor predeterminado ( True ), o bien cuando la opción
del compilador EmbedInteropTypes hace referencia al ensamblado. El tipo dynamic permite el enlace
en tiempo de ejecución, ya disponible en Visual Basic, y evita la conversión explícita que se requiere en
C# 3.0 y versiones anteriores del lenguaje.
Por ejemplo, excelApp.Columns[1] devuelve Object y AutoFit es un método Range de Excel. Sin
dynamic , debe convertir el objeto devuelto por excelApp.Columns[1] como una instancia de Range antes
de llamar al método AutoFit .
Para obtener más información sobre cómo insertar tipos de interoperabilidad, consulte los
procedimientos “Para buscar la referencia a un PIA” y “Para restaurar la dependencia de un PIA” más
adelante en este tema. Para obtener más información sobre dynamic , vea dynamic o Uso de tipo
dinámico.
Para invocar DisplayInExcel
1. Agregue el código siguiente al final del método ThisAddIn_StartUp . La llamada a DisplayInExcel
contiene dos argumentos. El primer argumento es el nombre de la lista de cuentas que se va a procesar.
El segundo argumento es una expresión lambda de varias líneas que define cómo se procesarán los
datos. Los valores ID y balance de cada cuenta se muestran en las celdas adyacentes y la fila se
muestra en rojo si el saldo es inferior a cero. Para obtener más información, vea Expresiones lambda.
2. Presione F5 para ejecutar el programa. Aparece una hoja de cálculo de Excel que contiene los datos de las
cuentas.
Para agregar un documento de Word
1. Agregue el código siguiente al final del método ThisAddIn_StartUp para crear un documento de Word
que contenga un vínculo al libro de Excel.
Este código muestra algunas de las nuevas características de C#: capacidad para omitir la palabra clave
ref en programación COM, argumentos con nombre y argumentos opcionales. Estas características ya
existen en Visual Basic. El método PasteSpecial tiene siete parámetros, y todos se definen como
parámetros de referencia opcionales. Los argumentos opcionales y con nombre permiten designar los
parámetros a los que se quiere tener acceso por nombre, y enviar argumentos únicamente a esos
parámetros. En este ejemplo se envían argumentos para indicar que se debe crear un vínculo al libro en
el Portapapeles (parámetro Link ), y que el vínculo se mostrará en el documento de Word como un
icono (parámetro DisplayAsIcon ). Visual C# también le permite omitir la palabra clave ref para estos
argumentos.
Para ejecutar la aplicación
1. Presione F5 para ejecutar la aplicación. Excel se abre y muestra una tabla que contiene la información de las
dos cuentas de bankAccounts . Entonces aparece un documento de Word que contiene un vínculo a la tabla
de Excel.
Para limpiar el proyecto completado
1. En Visual Studio, haga clic en Limpiar solución en el menú Compilación . De lo contrario, el complemento
se ejecutará cada vez que abra Excel en el equipo.
Para buscar la referencia a un PIA
1. Ejecute de nuevo la aplicación, pero no haga clic en Limpiar solución .
2. Seleccione Iniciar . Busque Microsoft Visual Studio <version> y abra un símbolo del sistema de
desarrollador.
3. Escriba ildasm en la ventana Símbolo del sistema para desarrolladores de Visual Studio y, luego,
presione ENTRAR. Aparecerá la ventana IL DASM.
4. En el menú Archivo de la ventana de IL DASM, seleccione Archivo > Abrir . Haga doble clic en
Visual Studio <version> y, de nuevo, en Proyectos . Abra la carpeta de su proyecto y, en la carpeta
bin/Debug, busque su_proyecto.dll. Haga doble clic en su_proyecto.dll. Una nueva ventana muestra los
atributos del proyecto, además de las referencias a otros módulos y ensamblados. Tenga en cuenta que
los espacios de nombres Microsoft.Office.Interop.Excel y Microsoft.Office.Interop.Word se incluyen
en el ensamblado. De manera predeterminada en Visual Studio, el compilador importa los tipos
necesarios desde un PIA con referencia a su ensamblado.
Para obtener más información, vea Cómo: Consulta del contenido de un ensamblado.
5. Haga doble clic en el icono MANIFIESTO . Aparecerá una ventana con una lista de ensamblados que
contienen los elementos a los que hace referencia el proyecto. Microsoft.Office.Interop.Excel y
Microsoft.Office.Interop.Word no están incluidos en la lista. Dado que los tipos que su proyecto necesita
se han importado en el ensamblado, las referencias a un PIA no son necesarias. Esto facilita la
implementación. Los PIA no tienen que estar presentes en el equipo del usuario y, puesto que una
aplicación no requiere la implementación de una versión concreta de un PIA, se pueden diseñar
aplicaciones que trabajen con varias versiones de Office, siempre que las API necesarias existan en todas
las versiones.
Dado que la implementación de los PIA ya no es necesaria, puede crear una aplicación en escenarios
avanzados que funcione con varias versiones de Office, incluidas versiones anteriores. Sin embargo, esto
solo funciona si el código no utiliza ninguna API que no esté disponible en la versión de Office con la que
está trabajando. No siempre está claro si una API concreta estaba disponible en una versión anterior y
por ese motivo no recomendamos trabajar con versiones anteriores de Office.
NOTE
Office no publicó ensamblados PIA antes de Office 2003. Por lo tanto, la única manera de generar un ensamblado
de interoperabilidad para Office 2002 o versiones anteriores es mediante la importación de la referencia COM.
Consulte también
Propiedades implementadas automáticamente (Visual Basic)
Propiedades autoimplementadas (C#)
Inicializadores de colección
Inicializadores de objeto y colección
Parámetros opcionales
Paso de argumentos por posición o por nombre
Argumentos opcionales y con nombre
Enlace en tiempo de compilación y en tiempo de ejecución
dynamic
Uso de tipo dinámico
Expresiones lambda (Visual Basic)
Expresiones lambda (C#)
Procedimiento para usar propiedades indizadas en la programación de interoperabilidad COM
Tutorial: Inserción de información de tipos de los ensamblados de Microsoft Office en Visual Studio
Tutorial: Inserción de tipos de ensamblados administrados
Tutorial: Creación del primer complemento VSTO para Excel
Interoperabilidad COM
Interoperabilidad
Clases COM de ejemplo (Guía de programación de
C#)
16/09/2021 • 2 minutes to read
El siguiente es un ejemplo de una clase que se expondría como un objeto COM. Una vez insertado este código
de ejemplo en un archivo .cs y agregado al proyecto, establezca la propiedad Registrar para
interoperabilidad COM en True . Para obtener más información, vea Cómo: Registrar un componente para
interoperabilidad COM.
Exponer objetos de Visual C# para COM requiere declarar una interfaz de clase, una interfaz de eventos si es
necesario y la propia clase. Los miembros de clase deben seguir estas reglas para que sean visibles en COM:
La clase debe ser pública.
Las propiedades, métodos y eventos deben ser públicos.
Las propiedades y métodos deben declararse en la interfaz de clase.
Los eventos deben declararse en la interfaz de eventos.
Los demás miembros públicos de la clase que no se declaren en estas interfaces no serán visibles para COM,
pero lo serán para el resto de objetos de .NET.
Para exponer propiedades y métodos en COM, se deben declarar en la interfaz de clase y marcar con el atributo
DispId , e implementarlos en la clase. El orden en que se declaran los miembros en la interfaz es el orden usado
para la tabla virtual de COM.
Para exponer los eventos de la clase, se deben declarar en la interfaz de eventos y marcarlos con un atributo
DispId . La clase no debe implementar esta interfaz.
La clase implementa la interfaz de clase y puede implementar más de una interfaz, pero la primera
implementación debe ser la interfaz de clase predeterminada. Implemente los métodos y propiedades expuestos
para COM aquí. Deben marcarse como públicos y coincidir con las declaraciones de la interfaz de clase.
Asimismo, declare los eventos iniciados por la clase aquí. Deben marcarse como públicos y coincidir con las
declaraciones de la interfaz de eventos.
Ejemplo
using System.Runtime.InteropServices;
namespace project_name
{
[Guid("EAA4976A-45C3-4BC5-BC0B-E474F4C3C83F")]
public interface ComClass1Interface
{
}
[Guid("7BD20046-DF8C-44A6-8F6B-687FAA26FA71"),
InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface ComClass1Events
{
}
[Guid("0D53A3E8-E51A-49C7-944E-E72A2064F938"),
ClassInterface(ClassInterfaceType.None),
ComSourceInterfaces(typeof(ComClass1Events))]
public class ComClass1 : ComClass1Interface
{
}
}
Consulte también
Guía de programación de C#
Interoperabilidad
Página Compilar (Diseñador de proyectos) (C#)
Referencia de C#
16/09/2021 • 4 minutes to read
En esta sección se proporciona material de referencia sobre palabras clave, operadores, caracteres especiales,
directivas de preprocesador, opciones del compilador y errores y advertencias del compilador de C#.
En esta sección
Palabras clave de C#
Ofrece vínculos para información sobre palabras clave de C# y sintaxis.
Operadores de C#
Ofrece vínculos para información sobre operadores de C# y sintaxis.
Caracteres especiales de C#
Proporciona vínculos a información sobre los caracteres especiales contextuales en C# y su uso.
Directivas de preprocesador de C#
Ofrece vínculos para información sobre los comandos del compilador para incrustar en código fuente de C#.
Opciones del compilador de C#
Incluye información sobre las opciones del compilador y cómo usarlas.
Errores del compilador de C#
Incluye fragmentos de código que muestran la causa y la corrección de errores y advertencias del compilador
de C#.
Especificación del lenguaje C#
Especificación del lenguaje C# 6.0 Se trata de un borrador de propuesta para el lenguaje C# 6.0. Este documento
se perfeccionará por medio del trabajo con el comité de normas de C# de ECMA. La versión 5.0 se ha publicado
en diciembre de 2017 como el documento Norma ECMA-334 estándar, quinta edición.
Las características que se han implementado en versiones de C# posteriores a 6.0 se representan en las
propuestas de especificación del lenguaje. Estos documentos describen los deltas de la especificación del
lenguaje con el fin de agregar estas nuevas características. Están en formato de propuesta de borrador. Estas
especificaciones se perfeccionarán y enviarán al comité de normas de ECMA para su revisión formal e
incorporación a una versión futura del estándar de C#.
Propuestas de especificación de C# 7.0
Hay una serie de nuevas características implementadas en C# 7.0. Entre ellas, está la coincidencia de patrones,
las funciones locales, las declaraciones de variable out, las expresiones throw, los literales binarios y los
separadores de dígitos. Esta carpeta contiene las especificaciones para cada una de esas características.
Propuestas de especificación de C# 7.1
Se han agregado nuevas características en C# 7.1. En primer lugar, puede escribir un método Main que
devuelva Task o Task<int> . Esto le permite agregar el modificador async a Main . La expresión default
puede usarse sin tipo en ubicaciones en las que sea posible inferir el tipo. Además, se pueden inferir los
nombres de los miembros de las tuplas. Por último, se puede usar la coincidencia de patrones con genéricos.
Propuestas de especificación de C# 7.2
Se han agregado una serie de pequeñas características a C# 7.2. Puede pasar argumentos por referencia de solo
lectura mediante la palabra clave in . Se han realizado diversos cambios de bajo nivel para admitir la seguridad
en tiempo de compilación para Span y tipos relacionados. Puede usar argumentos con nombre donde los
argumentos posteriores son posicionales, en algunos casos. El modificador de acceso private protected
permite especificar que los llamadores se limitan a los tipos derivados que se implementan en el mismo
ensamblado. El operador ?: se puede resolver en una referencia a una variable. También puede dar formato a
números hexadecimales y binarios con un separador de dígito inicial.
Propuestas de especificación de C# 7.3
C# 7.3 es otra versión secundaria que incluye una serie de pequeñas actualizaciones. Puede usar nuevas
restricciones en parámetros de tipo genérico. Otros cambios hacen que sea más fácil trabajar con campos
fixed , incluido el uso de asignaciones stackalloc . Las variables locales declaradas con la palabra clave ref
pueden reasignarse para que hagan referencia a un almacenamiento nuevo. Puede colocar atributos en
propiedades implementadas automáticamente que tengan como destino el campo de respaldo generado por el
compilador. Se pueden usar variables de expresión en inicializadores. Las tuplas pueden compararse para
comprobar si son iguales (o si no lo son). Además, se han realizado algunas mejoras en la resolución de
sobrecarga.
Propuestas de especificación de C# 8.0
C# 8.0 está disponible con .NET Core 3.0. Entre las características se incluyen tipos de referencia que aceptan
valores NULL, coincidencia de patrones recursiva, métodos de interfaz predeterminados, secuencias
asincrónicas, intervalos e índices, using basado en patrones y declaraciones using, asignación de uso combinado
de NULL y miembros de instancia de solo lectura.
Propuestas de especificaciones de C# 9.0
C# 9.0 está disponible con .NET 5.0. Entre las características disponibles se incluyen las siguientes: registros,
instrucciones de nivel superior, mejoras en la coincidencia de patrones, establecedores solo de inicialización,
nuevas expresiones con tipo de destino, inicializadores de módulos, ampliación de los métodos parciales,
funciones anónimas estáticas, expresiones condicionales con tipo de destino, tipos de retorno de covariantes,
extensión GetEnumerator en bucles Foreach, parámetros de descarte de expresiones lambda, atributos en
funciones locales, enteros de tamaño nativo, punteros de funciones, supresión de la emisión de marcas localsinit
y anotaciones de parámetros de tipos sin restricciones.
Secciones relacionadas
Uso del entorno de desarrollo de Visual Studio para C#
Ofrece vínculos para los temas de tareas y conceptos y temas que describen el IDE y el Editor.
Guía de programación de C#
Incluye información sobre cómo usar el lenguaje de programación de C#.
Control de versiones del lenguaje C#
16/09/2021 • 5 minutes to read
El compilador de C# más actualizado determina una versión de lenguaje predeterminada basada en los marcos
o las plataformas de destino del proyecto. Visual Studio no proporciona una interfaz de usuario para cambiar el
valor, pero se puede cambiar si se modifica el archivo csproj. La opción predeterminada garantiza que se use la
versión del lenguaje más reciente compatible con el marco de trabajo de destino. Se beneficia del acceso a las
características de lenguaje más recientes compatibles con el destino del proyecto. Esta opción predeterminada
también garantiza que no se use un lenguaje que requiera tipos o el comportamiento en tiempo de ejecución no
esté disponible en la plataforma de destino. La elección de una versión del lenguaje más reciente que la
predeterminada puede provocar errores en tiempo de compilación y en tiempo de ejecución difíciles de
diagnosticar.
Las reglas de este artículo se aplican al compilador ofrecido con Visual Studio 2019 o el SDK de .NET. Los
compiladores de C# que forman parte de la instalación de Visual Studio 2017 o versiones anteriores del SDK de
.NET Core tienen como destino C# 7.0 de forma predeterminada.
C# 8.0 solo se admite en .NET Core 3.x y versiones más recientes. Muchas de las características más recientes
requieren características de biblioteca y runtime introducidas en .NET Core 3.x:
La implementación de interfaz predeterminada requiere nuevas características en el CLR de .NET Core 3.0.
Las secuencias asincrónicas requieren los nuevos tipos System.IAsyncDisposable,
System.Collections.Generic.IAsyncEnumerable<T> y System.Collections.Generic.IAsyncEnumerator<T>.
Los índices y los intervalos requieren los nuevos tipos System.Indexy System.Range.
Los tipos de referencia que admiten un valor NULL hacen uso de varios atributos para proporcionar mejores
advertencias. Esos atributos se han agregado en .NET Core 3.0. Otras plataformas de destino no se han
anotado con ninguno de estos atributos. Esto significa que es posible que las advertencias que admiten un
valor NULL no reflejen con precisión los posibles problemas.
C# 9.0 solo se admite en .NET 5 y versiones más recientes.
Valores predeterminados
El compilador determina un valor predeterminado según estas reglas:
Cuando el proyecto tiene como destino un marco en versión preliminar que tenga una versión de lenguaje
preliminar correspondiente, la versión de lenguaje que se usa es la que está en versión preliminar. Puede usar
las características más recientes con esa versión preliminar en cualquier entorno, sin que afecte a los proyectos
que tienen como destino una versión de .NET Core publicada.
IMPORTANT
Visual Studio 2017 agregaba una entrada <LangVersion>latest</LangVersion> a los archivos de proyecto creados. Eso
implicaba el uso de C# 7.0 cuando esto sucedía. Sin embargo, una vez que se actualiza a Visual Studio 2019, se usa la
versión más reciente, sin importar la plataforma de destino. Estos proyectos ahora invalidan el comportamiento
predeterminado. Debe editar el archivo de proyecto y quitar ese nodo. Después, el proyecto usará la versión del
compilador recomendada para la plataforma de destino.
TIP
Para saber qué versión de lenguaje está usando actualmente, incluya #error version (con distinción de mayúsculas y
minúsculas) en el código. Esto hace que el compilador genere un error de compilador, CS8304, con un mensaje que
contiene la versión del compilador que se usa y la versión del lenguaje seleccionada actualmente. Vea #error (Referencia de
C#) para obtener más información.
<PropertyGroup>
<LangVersion>preview</LangVersion>
</PropertyGroup>
El valor preview usa la versión preliminar más reciente disponible del lenguaje C# que admite el compilador.
Configurar varios proyectos
Para configurar varios proyectos, se puede crear un archivo Director y.Build.props que contenga el elemento
<LangVersion> . Por lo general, esto se hace en el directorio de la solución. Agregue lo siguiente a un archivo
Director y.Build.props en el directorio de la solución:
<Project>
<PropertyGroup>
<LangVersion>preview</LangVersion>
</PropertyGroup>
</Project>
Las compilaciones de todos los subdirectorios del directorio que contenga ese archivo usarán la sintaxis de la
versión preliminar de C#. Para obtener más información, consulte Personalización de la compilación.
VA LO R SIGN IF IC A DO
Los tipos de valor y los tipos de referencia son las dos categorías principales de tipos de C#. Una variable de un
tipo de valor contiene una instancia del tipo. Esto difiere de una variable de un tipo de referencia, que contiene
una referencia a una instancia del tipo. De forma predeterminada, al asignar, pasar un argumento a un método
o devolver el resultado de un método, se copian los valores de variable. En el caso de las variables de tipo de
valor, se copian las instancias de tipo correspondientes. En el ejemplo siguiente se muestra ese comportamiento:
using System;
MutateAndDisplay(p2);
Console.WriteLine($"{nameof(p2)} after passing to a method: {p2}");
}
Como se muestra en el ejemplo anterior, las operaciones en una variable de tipo de valor solo afectan a esa
instancia del tipo de valor, almacenado en la variable.
Si un tipo de valor contiene un miembro de datos de un tipo de referencia, solo se copia la referencia a la
instancia del tipo de referencia al copiar una instancia de tipo de valor. Tanto la instancia de tipo de valor original
como la copia tienen acceso a la misma instancia de tipo de referencia. En el ejemplo siguiente se muestra ese
comportamiento:
using System;
using System.Collections.Generic;
public TaggedInteger(int n)
{
Number = n;
tags = new List<string>();
}
var n2 = n1;
n2.Number = 7;
n2.AddTag("B");
NOTE
Para que el código sea menos propenso a errores y más sólido, defina y use tipos de valor inmutables. En este artículo se
usan tipos de valor mutables solo con fines de demostración.
Vea también
Referencia de C#
System.ValueType
Tipos de referencia
Tipos numéricos enteros (referencia de C#)
16/09/2021 • 4 minutes to read
Los tipos numéricos integrales representan números enteros. Todos los tipos numéricos integrales son tipos de
valor. También son tipos simples y se pueden inicializar con literales. Todos los tipos numéricos enteros admiten
operadores aritméticos, lógicos bit a bit, de comparación y de igualdad.
PA L A B RA C L AVE/ T IP O DE
C# IN T ERVA LO TA M A Ñ O T IP O DE . N ET
En todas las filas de la tabla, excepto en las dos últimas, cada palabra clave de tipo de C# de la columna situada
más a la izquierda es un alias del tipo de .NET correspondiente. La palabra clave y el nombre de tipo de .NET son
intercambiables. Por ejemplo, en las declaraciones siguientes se declaran variables del mismo tipo:
int a = 123;
System.Int32 b = 123;
Los tipos nint y nuint de las dos últimas filas de la tabla son enteros de tamaño nativo. Se representan
internamente por los tipos de .NET indicados, pero, en cada caso, la palabra clave y el tipo de .NET no son
intercambiables. El compilador proporciona operaciones y conversiones para nint y nuint como tipos enteros
que no proporciona para los tipos de puntero System.IntPtr y System.UIntPtr . Para obtener más información,
consulte los tipos nint y nuint .
El valor predeterminado de cada tipo entero es cero, 0 . Cada uno de los tipos enteros, excepto los tipos de
tamaño nativo, tiene las constantes MinValue y MaxValue que proporcionan el valor mínimo y máximo de ese
tipo.
Use la estructura System.Numerics.BigInteger para representar un entero con signo sin límite superior ni
inferior.
Literales enteros
Los literales enteros pueden ser
decimales: sin ningún prefijo
hexadecimales: con el prefijo de 0x o 0X
binarios: con el prefijo 0b o 0B (disponible en C# 7.0 y versiones posteriores)
En el código siguiente se muestra un ejemplo de cada uno de ellos:
En el ejemplo anterior también se muestra el uso de _ como un separador de dígitos, que se admite a partir de
C# 7.0. Puede usar el separador de dígitos con todos los tipos de literales numéricos.
El tipo de un literal entero viene determinado por su sufijo, como se indica a continuación:
Si el literal no tiene sufijo, su tipo es el primero de los siguientes tipos en el que se puede representar su
valor: int , uint , long , ulong .
NOTE
Los literales se interpretan como valores positivos. Por ejemplo, el literal 0xFF_FF_FF_FF representa el número
4294967295 del tipo uint , aunque tiene la misma representación de bits que el número -1 del tipo int . Si
necesita un valor de un tipo determinado, convierta un literal en ese tipo. Use el operador unchecked si un valor
literal no se puede representar en el tipo de destino. Por ejemplo, unchecked((int)0xFF_FF_FF_FF) genera -1 .
Si un literal entero tiene el sufijo U o u , su tipo es el primero de los siguientes tipos en el que se puede
representar su valor: uint , ulong .
Si un literal entero tiene el sufijo L o l , su tipo es el primero de los siguientes tipos en el que se puede
representar su valor: long , ulong .
NOTE
Puede usar la letra minúscula l como sufijo. Sin embargo, esto genera una advertencia del compilador porque la
letra l se confunde fácilmente con el dígito 1 . Use L para mayor claridad.
byte a = 17;
byte b = 300; // CS0031: Constant value '300' cannot be converted to a 'byte'
Como se muestra en el ejemplo anterior, si el valor del literal no está dentro del intervalo del tipo de destino, se
produce el error CS0031 del compilador.
También puede usar una conversión para convertir el valor representado por un literal entero en un tipo que no
sea el determinado del literal:
Conversiones
Puede convertir un tipo numérico entero en cualquier otro tipo numérico entero. Si el tipo de destino puede
almacenar todos los valores del tipo de origen, la conversión es implícita. De lo contrario, debe usar una
expresión Cast para realizar una conversión explícita. Para obtener más información, consulte Conversiones
numéricas integradas.
Vea también
Referencia de C#
Tipos de valor
Tipos de punto flotante
Cadenas con formato numérico estándar
Valores numéricos en .NET
Tipos nint y nuint (Referencia de C#)
16/09/2021 • 2 minutes to read
A partir de C# 9.0, puede usar las palabras clave nint y nuint para definir enteros de tamaño nativo. Son
enteros de 32 bits cuando se ejecutan en un proceso de 32 bits, o bien enteros de 64 bits cuando se ejecutan en
un proceso de 64 bits. Se pueden usar para escenarios de interoperabilidad, bibliotecas de bajo nivel y para
optimizar el rendimiento en escenarios en los que se utilice la aritmética de enteros.
Los tipos enteros de tamaño nativo se representan internamente como los tipos System.IntPtr y System.UIntPtr
de .NET. A diferencia de otros tipos numéricos, las palabras clave no son simplemente alias de los tipos. Las
instrucciones siguientes son equivalentes:
nint a = 1;
System.IntPtr a = 1;
El compilador proporciona operaciones y conversiones para nint y nuint que son adecuadas para los tipos
enteros.
También puede obtener el valor equivalente de las propiedades estáticas IntPtr.Size y UIntPtr.Size.
MinValue y MaxValue
Para obtener los valores mínimo y máximo de los enteros de tamaño nativo en tiempo de ejecución, use
MinValue y MaxValue como propiedades estáticas con las palabras clave nint y nuint , como en el ejemplo
siguiente:
Console.WriteLine($"nint.MinValue = {nint.MinValue}");
Console.WriteLine($"nint.MaxValue = {nint.MaxValue}");
Console.WriteLine($"nuint.MinValue = {nuint.MinValue}");
Console.WriteLine($"nuint.MaxValue = {nuint.MaxValue}");
Constantes
Puede usar valores constantes en los rangos siguientes:
Para nint : entre Int32.MinValue y Int32.MaxValue.
Para nuint : entre UInt32.MinValue y UInt32.MaxValue.
Conversiones
El compilador proporciona conversiones implícitas y explícitas a otros tipos numéricos. Para obtener más
información, consulte Conversiones numéricas integradas.
Literales
No hay ninguna sintaxis directa para los literales de entero de tamaño nativo. No hay ningún sufijo para indicar
que un literal es un entero de tamaño nativo, como L para indicar long . En su lugar, puede usar conversiones
implícitas o explícitas de otros valores enteros. Por ejemplo:
nint a = 42
nint a = (nint)42;
Vea también
Referencia de C#
Tipos de valor
Tipos numéricos integrales
Conversiones numéricas integradas
Tipos numéricos de punto flotante (referencia de
C#)
16/09/2021 • 4 minutes to read
Los tipos numéricos de punto flotante representan números reales. Todos los tipos numéricos de punto flotante
son tipos de valor. También son tipos simples y se pueden inicializar con literales. Todos los tipos de punto
flotante numéricos admiten operadores aritméticos, de comparación y de igualdad.
PA L A B RA IN T ERVA LO
C L AVE/ T IP O DE C # A P RO XIM A DO P REC ISIÓ N TA M A Ñ O T IP O DE . N ET
En la tabla anterior, cada palabra clave de tipo de C# de la columna ubicada más a la izquierda es un alias del
tipo de .NET correspondiente. Son intercambiables. Por ejemplo, en las declaraciones siguientes se declaran
variables del mismo tipo:
double a = 12.3;
System.Double b = 12.3;
El valor predeterminado de cada tipo de punto flotante es cero, 0 . Cada uno de los tipos de punto flotante tiene
las constantes MinValue y MaxValue que proporcionan el valor finito mínimo y máximo de ese tipo. Los tipos
float y double también brindan constantes que representan valores infinitos y no numéricos. Por ejemplo, el
tipo double ofrece las siguientes constantes: Double.NaN, Double.NegativeInfinity y Double.PositiveInfinity.
El tipo de decimal es adecuado cuando el grado de precisión requerido viene determinado por el número de
dígitos a la derecha del separador decimal. Estos números se utilizan normalmente en aplicaciones financieras,
para importes monetarios (por ejemplo, 1,00 $), tasas de interés (por ejemplo, 2,625 %), etc. Los números pares
que son precisos únicamente para un dígito decimal se controlan de forma más precisa en el tipo decimal : 0,1,
por ejemplo, se puede representar exactamente mediante una instancia de decimal , mientras que no hay una
instancia double o float que representa exactamente 0,1. Debido a esta diferencia en los tipos numéricos, se
pueden producir errores de redondeo inesperados en cálculos aritméticos cuando se usa double o float para
datos decimales. Puede usar double en lugar de decimal cuando optimizar el rendimiento es más importante
que asegurar la precisión. Sin embargo, cualquier diferencia de rendimiento pasaría desapercibida para todas
las aplicaciones, salvo las que consumen más cálculos. Otra posible razón para evitar decimal es minimizar los
requisitos de almacenamiento. Por ejemplo, ML.NET usa float porque la diferencia entre 4 bytes y 16 bytes se
acumula para conjuntos de datos muy grandes. Para obtener más información, vea System.Decimal.
En una expresión, puede combinar tipos enteros y tipos float y double . En este caso, los tipos enteros se
convierten implícitamente en uno de los tipos de punto flotante y, si es necesario, el tipo float se convierte
implícitamente en double . La expresión se evalúa de la siguiente forma:
Si hay un tipo double en la expresión, esta se evalúa como double , o bien como bool en comparaciones
relacionales o de igualdad.
Si no hay un tipo double en la expresión, esta se evalúa como float , o bien como bool en comparaciones
relacionales o de igualdad.
También es posible combinar en una expresión tipos enteros y el tipo decimal . En este caso, los tipos enteros se
convierten implícitamente en el tipo decimal y la expresión se evalúa como decimal , o bien como bool en
comparaciones relacionales y de igualdad.
En una expresión, no se puede mezclar el tipo decimal con los tipos float y double . En este caso, si quiere
realizar operaciones aritméticas, de comparación o de igualdad, debe convertir explícitamente los operandos del
tipo decimal o a este mismo tipo, como se muestra en el ejemplo siguiente:
double a = 1.0;
decimal b = 2.1m;
Console.WriteLine(a + (double)b);
Console.WriteLine((decimal)a + b);
Puede usar cadenas con formato numérico estándar o cadenas con formato numérico personalizado para dar
formato a un valor de punto flotante.
Literales reales
El tipo de un literal real viene determinado por su sufijo, como se indica a continuación:
El literal sin sufijo o con el sufijo d o D es del tipo double
El literal con el sufijo f o F es del tipo float
El literal con el sufijo m o M es del tipo decimal
En el código siguiente se muestra un ejemplo de cada uno de ellos:
double d = 3D;
d = 4d;
d = 3.934_001;
float f = 3_000.5F;
f = 5.4f;
En el ejemplo anterior también se muestra el uso de _ como un separador de dígitos, que se admite a partir de
C# 7.0. Puede usar el separador de dígitos con todos los tipos de literales numéricos.
También puede usar la notación científica; es decir, especificar una parte exponencial de un literal real, como se
muestra en el ejemplo siguiente:
double d = 0.42e2;
Console.WriteLine(d); // output 42
float f = 134.45E-2f;
Console.WriteLine(f); // output: 1.3445
decimal m = 1.5E6m;
Console.WriteLine(m); // output: 1500000
Conversiones
Solo hay una conversión implícita entre los tipos numéricos de punto flotante: de float a double . Sin
embargo, puede convertir un tipo de punto flotante a cualquier otro tipo de punto flotante con la conversión
explícita. Para obtener más información, consulte Conversiones numéricas integradas.
Vea también
Referencia de C#
Tipos de valor
Tipos enteros
Cadenas con formato numérico estándar
Valores numéricos en .NET
System.Numerics.Complex
Conversiones numéricas integradas (referencia de
C#)
16/09/2021 • 4 minutes to read
C# proporciona un conjunto de tipos numéricos enteros y de punto flotante. Existe una conversión entre dos
tipos numéricos, ya sea implícita o explícita. Debe usar una expresión Cast para realizar una conversión explícita.
DE EN
float double
NOTE
Las conversiones implícitas de int , uint , long , ulong , nint o nuint a float y de long , ulong , nint o
nuint a double pueden provocar una pérdida de precisión, pero nunca una pérdida de un orden de magnitud. El resto
de conversiones numéricas implícitas nunca pierden información.
byte a = 13;
byte b = 300; // CS0031: Constant value '300' cannot be converted to a 'byte'
Como se muestra en el ejemplo anterior, si el valor constante no está dentro del rango del tipo de
destino, se produce el error del compilador CS0031.
DE EN
byte sbyte
NOTE
Una conversión numérica explícita podría provocar la pérdida de datos o producir una excepción, normalmente una de
tipo OverflowException.
Vea también
Referencia de C#
Conversiones de tipos
bool (Referencia de C#)
16/09/2021 • 2 minutes to read
La palabra clave de tipo bool es un alias para el tipo de estructura de .NET System.Boolean que representa un
valor booleano que puede ser true o false .
Para realizar operaciones lógicas con valores del tipo bool , use operadores lógicos booleanos. El tipo bool es
el tipo de resultado de los operadores de comparación e igualdad. Una expresión bool puede ser una expresión
condicional de control en las instrucciones if, do, while y for, así como en el operador condicional ?: .
El valor predeterminado del tipo bool es false .
Literales
Puede usar los literales true y false para inicializar una variable bool o para pasar un valor bool :
Conversiones
C# solo proporciona dos conversiones que implican al tipo bool . Son una conversión implícita al tipo bool?
que acepta valores NULL correspondiente y una conversión explícita del tipo bool? . Sin embargo, .NET
proporciona métodos adicionales que se pueden usar para realizar una conversión al tipo bool , o bien
revertirla. Para obtener más información, vea la sección Convertir a y desde valores booleanos de la página de
referencia de la API System.Boolean.
Vea también
Referencia de C#
Tipos de valor
operadores true y false
char (referencia de C#)
16/09/2021 • 2 minutes to read
La palabra clave de tipo char es un alias para el tipo de estructura de .NET System.Char que representa un
carácter Unicode UTF-16.
T IP O IN T ERVA LO TA M A Ñ O T IP O DE . N ET
Literales
Puede especificar un valor de char con:
un literal de carácter.
una secuencia de escape Unicode, que es \u seguido de la representación hexadecimal de cuatro símbolos
de un código de carácter.
una secuencia de escape hexadecimal, que es \x seguido de la representación hexadecimal de un código de
carácter.
Como se muestra en el ejemplo anterior, también puede convertir el valor de un código de carácter en el valor
de char correspondiente.
NOTE
En el caso de una secuencia de escape Unicode, debe especificar los cuatro dígitos hexadecimales. Es decir, \u006A es
una secuencia de escape válida, mientras que \u06A y \u6A no son válidas.
En el caso de una secuencia de escape hexadecimal, puede omitir los ceros a la izquierda. Es decir, las secuencias de escape
\x006A , \x06A y \x6A son válidas y se corresponden con el mismo carácter.
Conversiones
El tipo char se puede convertir implícitamente en los tipos enteros siguientes: ushort , int , uint , long y
ulong . También se puede convertir implícitamente en los tipos numéricos de punto flotante integrados: float ,
double y decimal . Se puede convertir explícitamente en los tipos enteros sbyte , byte y short .
No hay ninguna conversión implícita de otros tipos al tipo char . Sin embargo, cualquier tipo numérico entero o
de punto flotante es implícitamente convertible a char .
Vea también
Referencia de C#
Tipos de valor
Cadenas
System.Text.Rune
Codificación de caracteres de .NET
Tipos de enumeración (referencia de C#)
16/09/2021 • 4 minutes to read
Un tipo de enumeración es un tipo de valor definido por un conjunto de constantes con nombre del tipo
numérico integral subyacente. Para definir un tipo de enumeración, use la palabra clave enum y especifique los
nombres de miembros de enumeración:
enum Season
{
Spring,
Summer,
Autumn,
Winter
}
De forma predeterminada, los valores de constante asociados de miembros de enumeración son del tipo int ;
comienzan con cero y aumentan en uno después del orden del texto de la definición. Puede especificar
explícitamente cualquier otro tipo de numérico entero como un tipo subyacente de un tipo de enumeración.
También puede especificar explícitamente los valores de constante asociados, como se muestra en el ejemplo
siguiente:
No se puede definir un método dentro de la definición de un tipo de enumeración. Para agregar funcionalidad a
un tipo de enumeración, cree un método de extensión.
El valor predeterminado de un tipo de enumeración E es el valor generado por la expresión (E)0 , incluso si
cero no tiene el miembro de enumeración correspondiente.
Un tipo de enumeración se usa para representar una opción de un conjunto de valores mutuamente excluyentes
o una combinación de opciones. Para representar una combinación de opciones, defina un tipo de enumeración
como marcas de bits.
var a = (Days)37;
Console.WriteLine(a);
// Output:
// Monday, Wednesday, Saturday
}
}
Para más información y ver algunos ejemplos, consulte la página de referencia de API de System.FlagsAttribute
y la sección miembros no exclusivos y el atributo Flags de la página de referencia de API de System.Enum.
Conversiones
Para cualquier tipo de enumeración, existen conversiones explícitas entre el tipo de enumeración y su tipo
entero subyacente. Si convierte (usacast) un valor de enumeración a su tipo subyacente, el resultado es el valor
entero asociado de un miembro de enumeración.
public enum Season
{
Spring,
Summer,
Autumn,
Winter
}
var b = (Season)1;
Console.WriteLine(b); // output: Summer
var c = (Season)4;
Console.WriteLine(c); // output: 4
}
}
Vea también
Referencia de C#
Cadenas de formato de enumeración
Instrucciones de diseño: diseño de enumeraciones
Instrucciones de diseño: convenciones de nomenclatura de enumeración
Expresión switch
Instrucción switch
Tipos de estructura (Referencia de C#)
16/09/2021 • 8 minutes to read
Un tipo de estructura (o tipo struct) es un tipo de valor que puede encapsular datos y funcionalidad relacionada.
Para definir un tipo de estructura se usa la palabra clave struct :
Los tipos de estructura tienen semántica de valores. Es decir, una variable de un tipo de estructura contiene una
instancia del tipo. De forma predeterminada, los valores de variable se copian al asignar, pasar un argumento a
un método o devolver el resultado de un método. En el caso de una variable de tipo de estructura, se copia una
instancia del tipo. Para más información, vea Tipos de valor.
Normalmente, los tipos de estructura se usan para diseñar tipos de pequeño tamaño centrados en datos que
proporcionan poco o ningún comportamiento. Por ejemplo, en .NET se usan los tipos de estructura para
representar un número (entero y real), un valor booleano, un caracter Unicode, una instancia de tiempo. Si le
interesa el comportamiento de un tipo, considere la posibilidad de definir una clase. Los tipos de clase tienen
semántica de referencias. Es decir, una variable de un tipo de clase contiene una referencia a una instancia del
tipo, no la propia instancia.
Dado que los tipos de estructura tienen semántica del valor, le recomendamos que defina tipos de estructura
inmutables.
Estructura readonly
A partir de C# 7.2, se usa el modificador readonly para declarar que un tipo de estructura es inmutable. Todos
los miembros de datos de una estructura readonly debe ser de solo lectura tal como se indica a continuación:
Cualquier declaración de campo debe tener el modificador readonly
Cualquier propiedad, incluidas las implementadas automáticamente, debe ser de solo lectura. En C# 9.0 y
versiones posteriores, una propiedad puede tener un descriptor de acceso init .
Esto garantiza que ningún miembro de una estructura readonly modifique el estado de la misma. En C# 8.0 y
en versiones posteriores, eso significa que otros miembros de instancia, excepto los constructores, son
implícitamente readonly .
NOTE
En una estructura readonly , un miembro de datos de un tipo de referencia mutable puede seguir mutando su propio
estado. Por ejemplo, no puede reemplazar una instancia de List<T>, pero puede agregarle nuevos elementos.
En el código siguiente se define una estructura readonly con establecedores de propiedad de solo inicialización,
disponibles en C# 9.0 y versiones posteriores:
También puede aplicar el modificador readonly a los métodos que invalidan los métodos declarados en
System.Object:
Propiedades e indizadores:
Si tiene que aplicar el modificador readonly a los dos descriptores de acceso de una propiedad o un
indizador, aplíquelo en la declaración de la propiedad o el indizador.
NOTE
El compilador declara un descriptor de acceso get de una propiedad implementada automáticamente como
readonly , con independencia de la presencia del modificador readonly en la declaración de una propiedad.
En C# 9.0 y versiones posteriores, puede aplicar el modificador readonly a una propiedad o un indizador
con un descriptor de acceso init :
En el caso de los tipos de valor integrados, use los literales correspondientes para especificar un valor del tipo.
Estructura ref
A partir de C# 7.2, puede usar el modificador ref en la declaración de un tipo de estructura. Las instancias de
un tipo de estructura ref se asignan en la pila y no pueden escapar al montón administrado. Para asegurarse
de eso, el compilador limita el uso de tipos de estructura ref de la manera siguiente:
Una estructura ref no puede ser el tipo de elemento de una matriz.
Una estructura ref no puede ser un tipo declarado de un campo de una clase o una estructura que no sea
ref .
Una estructura ref no puede implementar interfaces.
En una estructura ref no se puede aplicar una conversión boxing a System.ValueType ni System.Object.
Una estructura ref no puede ser un argumento de tipo.
Una ref variable de estructura no se puede capturar mediante una expresión lambda o una función local.
Una variable de estructura ref no se puede usar en un método async . Aunque se pueden usar variables de
estructura ref en métodos sincrónicos, como, por ejemplo, en los que devuelven Task o Task<TResult>.
Una variable de estructura ref no se puede usar en iteradores.
Normalmente, se define un tipo de estructura ref cuando se necesita un tipo que también incluye miembros
de datos de tipos de estructura ref :
public ref struct CustomRef
{
public bool IsValid;
public Span<int> Inputs;
public Span<int> Outputs;
}
Para declarar una estructura ref como readonly , combine los modificadores readonly y ref en la
declaración de tipos (el modificador readonly debe ir delante del modificador ref ):
Restricción struct
Use también la palabra clave struct de la restricción struct para especificar que un parámetro de tipo es un
tipo de valor que no acepta valores NULL. Los tipos de estructura y enumeración satisfacen la restricción
struct .
Conversiones
Para cualquier tipo de estructura (excepto los tipos de estructura ref ), existen conversiones boxing y unboxing
a y desde los tipos System.ValueType y System.Object. También existen conversiones boxing y unboxing entre
un tipo de estructura y cualquier interfaz que implemente.
Vea también
Referencia de C#
Instrucciones de diseño: elección entre clase y estructura
Instrucciones de diseño: diseño de estructuras
Clases, estructuras y registros
Tipos de tupla (referencia de C#)
16/09/2021 • 9 minutes to read
Disponible en C# 7.0 y versiones posteriores, la característica tuplas proporciona una sintaxis concisa para
agrupar varios elementos de datos en una estructura de datos ligera. En el siguiente ejemplo se muestra cómo
se puede declarar una variable de tupla, inicializarla y acceder a sus miembros de datos:
Como se muestra en el ejemplo anterior, para definir un tipo de tupla, se especifican los tipos de todos sus
miembros de datos y, opcionalmente, los nombres de campos. No se pueden definir métodos en un tipo de
tupla, pero se pueden usar los métodos proporcionados por .NET, como se muestra en el siguiente ejemplo:
A partir de C# 7.3, los tipos de tupla admiten operadores de igualdad == y != . Para obtener más información,
consulte la sección Igualdad de tupla.
Los tipos de tupla son tipos de valores; los elementos de tupla son campos públicos. Esto hace que las tuplas
sean tipos de valor mutables.
NOTE
La característica de las tuplas requiere el tipo System.ValueTuple y los tipos genéricos relacionados (por ejemplo,
System.ValueTuple<T1,T2>), que están disponibles en .NET Core y .NET Framework 4.7 y versiones posteriores. Para usar
tuplas en un proyecto que tenga como destino .NET Framework 4.6.2 o versiones anteriores, agregue el paquete NuGet
System.ValueTuple al proyecto.
var t =
(1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 15, 16, 17, 18,
19, 20, 21, 22, 23, 24, 25, 26);
Console.WriteLine(t.Item26); // output: 26
var xs = new[] { 4, 7, 9 };
var limits = FindMinMax(xs);
Console.WriteLine($"Limits of [{string.Join(" ", xs)}] are {limits.min} and {limits.max}");
// Output:
// Limits of [4 7 9] are 4 and 9
Como se muestra en el ejemplo anterior, puede trabajar directamente con la instancia de la tupla devuelta o
deconstruirla en variables independientes.
También puede utilizar tipos de tupla en lugar de tipos anónimos; por ejemplo, en las consultas LINQ. Para
obtener más información, vea Elección entre tipos de tupla y anónimos.
Normalmente, se usan tuplas para agrupar elementos de datos relacionados de forma flexible. Esto suele ser útil
en métodos de utilidad privados e internos. En el caso de la API pública, considere la posibilidad de definir un
tipo de clase o de estructura.
A partir de C# 7.1, si no se especifica ningún nombre de campo, se puede deducir del nombre de la variable
correspondiente en una expresión de inicialización de tupla, como se muestra en el siguiente ejemplo:
Esto se conoce como "inicializadores de proyección de tupla". El nombre de una variable no se proyecta en un
nombre de campo de tupla en los siguientes casos:
El nombre del candidato es un nombre de miembro de un tipo de tupla, por ejemplo, Item3 , ToString o
Rest .
El nombre del candidato es un duplicado de otro nombre de campo de tupla, ya sea explícita o implícita.
En estos casos, se especifica el nombre de un campo o se accede a un campo por su nombre predeterminado.
Los nombres predeterminados de los campos de tupla son Item1 , Item2 , Item3 , etc. Siempre puede usar el
nombre predeterminado de un campo, incluso cuando se especifica un nombre de campo de forma explícita o
inferida, como se muestra en el siguiente ejemplo:
var a = 1;
var t = (a, b: 2, 3);
Console.WriteLine($"The 1st element is {t.Item1} (same as {t.a}).");
Console.WriteLine($"The 2nd element is {t.Item2} (same as {t.b}).");
Console.WriteLine($"The 3rd element is {t.Item3}.");
// Output:
// The 1st element is 1 (same as 1).
// The 2nd element is 2 (same as 2).
// The 3rd element is 3.
En la asignación de tuplas y las comparaciones de igualdad de tuplas no se tienen en cuenta los nombres de
campo.
En el tiempo de compilación, el compilador sustituye los nombres de campo no predeterminados por los
nombres predeterminados correspondientes. Como resultado, los nombres de campo especificados o inferidos
no están disponibles en el tiempo de ejecución.
También puede usar el operador de asignación = para deconstruir una instancia de tupla en variables
independientes. Para ello, siga uno de estos métodos:
Declarar explícitamente el tipo de cada variable entre paréntesis:
Usar la palabra clave var fuera de los paréntesis para declarar las variables con tipo implícito y permitir
que el compilador deduzca sus tipos:
Para obtener más información sobre la deconstrucción de tuplas y otros tipos, consulte Deconstrucción de
tuplas y otros tipos.
Igualdad de tupla
Comenzando con C# 7.3, los tipos de tupla admiten los operadores == y != . Estos operadores comparan los
miembros del operando izquierdo con los miembros correspondientes del operando derecho, siguiendo el
orden de los elementos de la tupla.
(int a, byte b) left = (5, 10);
(long a, int b) right = (5, 10);
Console.WriteLine(left == right); // output: True
Console.WriteLine(left != right); // output: False
Como se muestra en el ejemplo anterior, las operaciones == y != no tienen en cuenta los nombres de campo
de tupla.
Dos tuplas son comparables cuando se cumplen estas dos condiciones:
Ambas tuplas tienen el mismo número de elementos. Por ejemplo, t1 != t2 no se compila si t1 y t2
tienen números diferentes de elementos.
Para cada posición de tupla, los elementos correspondientes de los operandos de la tupla de la izquierda y de
la derecha son comparables con los operadores == y != . Por ejemplo, (1, (2, 3)) == ((1, 2), 3) no se
compila porque 1 no es comparable con (1, 2) .
Los operadores == y != comparan las tuplas en modo de cortocircuito. Es decir, una operación se detiene en
cuanto da con un par de elementos que no son iguales o alcanza los extremos de las tuplas. Sin embargo, antes
de cualquier comparación, se evalúan todos los elementos de tupla, como se muestra en el siguiente ejemplo:
int Display(int s)
{
Console.WriteLine(s);
return s;
}
// Output:
// 1
// 2
// 3
// 4
// False
Vea también
Referencia de C#
Tipos de valor
Elección entre tipos de tupla y anónimos
System.ValueTuple
Tipos de valor que admiten valores NULL
(referencia de C#)
16/09/2021 • 9 minutes to read
Un tipo de valor que admite un valor NULL T? representa todos los valores de su tipo de valor subyacente T y
un valor NULL adicional. Por ejemplo, puede asignar cualquiera de los tres valores siguientes a una variable
bool? : true , false o null . Un tipo de valor subyacente T no puede ser un tipo de valor que acepte valores
NULL por sí mismo.
NOTE
C# 8.0 presenta la característica de tipos de referencia que admiten un valor NULL. Para más información, consulte Tipos
de referencia que admiten un valor NULL. Los tipos de valor que admiten valores NULL están disponibles a partir de C# 2.
Todos los tipos que admiten valores NULL son instancias de la estructura System.Nullable<T> genérica. Puede
hacer referencia a un tipo de valor que admite valores NULL con un tipo subyacente T en cualquiera de las
formas intercambiables siguientes: Nullable<T> o T? .
Normalmente, los tipos de valor que admiten valores NULL se usan cuando es necesario representar el valor
indefinido de un tipo de valor subyacente. Por ejemplo, una variable booleana, o bool , solo puede ser true o
false . Sin embargo, en algunas aplicaciones, un valor de variable puede estar sin definir o faltar. Por ejemplo,
un campo de base de datos puede contener true o false , o puede no contener ningún valor, es decir, NULL .
Puede usar el tipo bool? en ese escenario.
Declaración y asignación
Como un tipo de valor se puede convertir de forma implícita al tipo de valor que admite valores NULL
correspondiente, un valor se puede asignar a un tipo que admite valores NULL como se haría para su tipo de
valor subyacente. También se puede asignar el valor null . Por ejemplo:
double? pi = 3.14;
char? letter = 'a';
int m2 = 10;
int? m = m2;
El valor predeterminado de un tipo de valor que admite valores NULL se representa como null , es decir, es una
instancia cuya propiedad Nullable<T>.HasValue devuelve false .
Siempre puede usar las siguientes propiedades de solo lectura para examinar y obtener un valor de una variable
de tipo de valor que admite valores NULL:
Nullable<T>.HasValue indica si una instancia de un tipo que admite valores NULL tiene un valor de su
tipo subyacente.
Nullable<T>.Value obtiene el valor de un tipo subyacente si HasValue es true . Si HasValue es false , la
propiedad Value inicia una excepción InvalidOperationException.
En el ejemplo siguiente se usa la propiedad HasValue para comprobar si la variable contiene un valor antes de
mostrarlo:
int? b = 10;
if (b.HasValue)
{
Console.WriteLine($"b is {b.Value}");
}
else
{
Console.WriteLine("b does not have a value");
}
// Output:
// b is 10
También se puede comparar una variable de un tipo de valor que admite valores NULL con null en lugar de
usar la propiedad HasValue , como se muestra en el ejemplo siguiente:
int? c = 7;
if (c != null)
{
Console.WriteLine($"c is {c.Value}");
}
else
{
Console.WriteLine("c does not have a value");
}
// Output:
// c is 7
int? c = null;
int d = c ?? -1;
Console.WriteLine($"d is {d}"); // output: d is -1
Si desea utilizar el valor predeterminado del tipo de valor subyacente en lugar de null , use el método
Nullable<T>.GetValueOrDefault().
También puede convertir de forma explícita un tipo de valor que admite valores NULL en uno que no los admite,
como se indica en el ejemplo siguiente:
int? n = null;
En tiempo de ejecución, si el valor de un tipo de valor que admite un valor NULL es null , la conversión explícita
inicia una InvalidOperationException.
Un tipo de valor que no admite valores NULL T se convierte implícitamente al tipo que admite valores NULL
T? correspondiente.
Operadores de elevación
Los operadores unarios y binarios predefinidos o los operadores sobrecargados que admite un tipo de valor T
también se admiten en el tipo de valor que admite un valor NULL T? correspondiente. Estos operadores,
también conocidos como operadores de elevación , generan un valor null si uno o los dos operandos son
null ; de lo contrario, el operador usa los valores contenidos de sus operandos para calcular el resultado. Por
ejemplo:
int? a = 10;
int? b = null;
int? c = 10;
a++; // a is 11
a = a * c; // a is 110
a = a + b; // a is null
NOTE
Para el tipo bool? , los operadores predefinidos & y | no siguen las reglas descritas en esta sección: el resultado de
una evaluación de operador puede ser distinto de NULL incluso si uno de los operandos es null . Para más información,
consulte la sección Operadores lógicos booleanos que aceptan valores NULL del artículo Operadores lógicos booleanos.
En el caso de los operadores de comparación < , > , <= y >= , si uno o ambos operandos son null , el
resultado es false ; de lo contrario, se comparan los valores contenidos de los operandos. No asuma que, como
una comparación determinada (por ejemplo, <= ) devuelve false , la comparación contraria ( > ) devolverá
true . En el ejemplo siguiente se muestra que 10 no es
int? b = null;
int? c = null;
Console.WriteLine($"null >= null is {b >= c}");
Console.WriteLine($"null == null is {b == c}");
// Output:
// null >= null is False
// null == null is True
En el caso de los operadores de igualdad == , si ambos operandos son null , el resultado es true ; si solo uno
de los operandos es null , el resultado es false ; de lo contrario, se comparan los valores contenidos de los
operandos.
En el caso de los operadores de desigualdad != , si ambos operandos son null , el resultado es false ; si solo
uno de los operandos es null , el resultado es true ; de lo contrario, se comparan los valores contenidos de los
operandos.
Si existe una conversión definida por el usuario entre dos tipos de valor, esa misma conversión también se
puede usar entre los correspondientes tipos que aceptan valores NULL.
Se puede aplicar conversión unboxing a un tipo de valor T con conversión boxing al tipo T? que acepta
valores NULL correspondiente, como se muestra en el ejemplo siguiente:
int a = 41;
object aBoxed = a;
int? aNullable = (int?)aBoxed;
Console.WriteLine($"Value of aNullable: {aNullable}");
// Output:
// int? is nullable value type
// int is non-nullable value type
Como se muestra en el ejemplo, se usa el operador typeof para crear una instancia System.Type.
Si quiere determinar si una instancia es de un tipo de valor que admite un valor NULL, no use el método
Object.GetType para obtener una instancia de Type para probarla con el código anterior. Cuando se llama al
método Object.GetType en una instancia de un tipo de valor que admite un valor NULL, se aplica la conversión
boxing a la instancia para convertirla en Object. Como la conversión boxing de una instancia que no es NULL de
un tipo de valor que admite valores NULL equivale a la conversión boxing de un valor del tipo subyacente,
GetType devuelve una instancia Type que representa el tipo de valor subyacente que admite valores NULL:
int? a = 17;
Type typeOfA = a.GetType();
Console.WriteLine(typeOfA.FullName);
// Output:
// System.Int32
No use el operador is para determinar si una instancia es de un tipo de valor que admite valores NULL. Como se
muestra en el ejemplo siguiente, no se pueden distinguir los tipos de instancias de un tipo de valor que admite
valores NULL y su tipo subyacente mediante el operador is :
int? a = 14;
if (a is int)
{
Console.WriteLine("int? instance is compatible with int");
}
int b = 17;
if (b is int?)
{
Console.WriteLine("int instance is compatible with int?");
}
// Output:
// int? instance is compatible with int
// int instance is compatible with int?
Se puede usar el código que se presenta en el ejemplo siguiente para determinar si una instancia es de un tipo
de valor que admite un valor NULL:
int? a = 14;
Console.WriteLine(IsOfNullableType(a)); // output: True
int b = 17;
Console.WriteLine(IsOfNullableType(b)); // output: False
bool IsOfNullableType<T>(T o)
{
var type = typeof(T);
return Nullable.GetUnderlyingType(type) != null;
}
NOTE
Los métodos que se describen en esta sección no son aplicables en el caso de los tipos de referencia nula.
Vea también
Referencia de C#
¿Qué significa exactamente "elevado"?
System.Nullable<T>
System.Nullable
Nullable.GetUnderlyingType
Tipos de referencia que aceptan valores null
Tipos de referencia (referencia de C#)
16/09/2021 • 2 minutes to read
Hay dos clases de tipos en C#: tipos de referencia y tipos de valor. Las variables de tipos de referencia almacenan
referencias en sus datos (objetos), mientras que las variables de tipos de valor contienen directamente los datos.
Con los tipos de referencia, dos variables pueden hacer referencia al mismo objeto y, por lo tanto, las
operaciones en una variable pueden afectar al objeto al que hace referencia la otra variable. Con los tipos de
valor, cada variable tiene su propia copia de los datos, y no es posible que las operaciones en una variable
afecten a la otra (excepto en el caso de las variables de parámetro in, ref y out, consulte el modificador de
parámetro in, ref y out).
Las palabras clave siguientes se usan para declarar tipos de referencia:
class
interface
delegate
record
C# también proporciona los siguientes tipos de referencia integrados:
dynamic
object
string
Vea también
Referencia de C#
Palabras clave de C#
Tipos de puntero
Tipos de valor
Tipos de referencia integrados (referencia de C#)
16/09/2021 • 8 minutes to read
C# tiene un número de tipos de referencia integrados. Tienen palabras clave u operadores que son sinónimos
para un tipo en la biblioteca de .NET.
El tipo de objeto
El tipo object es un alias de System.Object en .NET. En el sistema de tipos unificado de C#, todos los tipos, los
predefinidos y los definidos por el usuario, los tipos de referencia y los tipos de valores, heredan directa o
indirectamente de System.Object. Puede asignar valores de cualquier tipo a las variables de tipo object .
Cualquier variable object puede asignarse a su valor predeterminado con el literal null . Cuando una variable
de un tipo de valor se convierte en objeto, se dice que se aplica la conversión boxing. Cuando una variable de
tipo object se convierte en un tipo de valor, se dice que se aplica la conversión unboxing. Para obtener más
información, vea Conversión boxing y unboxing.
Tipo string
El tipo string representa una secuencia de cero o más caracteres Unicode. string es un alias de System.String
en .NET.
Aunque string es un tipo de referencia, se definen los operadores de igualdad == y != para comparar los
valores de los objetos string , no las referencias. Esto hace que las pruebas de igualdad entre cadenas sean más
intuitivas. Por ejemplo:
string a = "hello";
string b = "h";
// Append to contents of 'b'
b += "ello";
Console.WriteLine(a == b);
Console.WriteLine(object.ReferenceEquals(a, b));
Se muestra "True" y luego "False" porque el contenido de las cadenas es equivalente, pero a y b no hacen
referencia a la misma instancia de cadena.
El operador + concatena cadenas:
string b = "h";
b += "ello";
El operador [] puede usarse para el acceso de solo lectura a determinados caracteres de una cadena. Los
valores válidos comienzan por 0 y deben ser menores que la longitud de la cadena:
De igual manera, el operador [] también puede usarse para recorrer en iteración cada carácter en una cadena:
Los literales de cadena son del tipo string y se pueden escribir de dos formas, entre comillas y entre @ . Los
literales de cadena se incluyen entre comillas dobles ("):
Los literales de cadena pueden contener cualquier literal de carácter. Se incluyen secuencias de escape. En el
ejemplo siguiente se usa una secuencia de escape \\ para la barra diagonal inversa, \u0066 para la letra f y
\n para la nueva línea.
NOTE
El código de escape \udddd (donde dddd es un número de cuatro dígitos) representa el carácter Unicode U+ dddd .
También se reconocen los códigos de escape Unicode de 8 dígitos: \Udddddddd .
Los literales de cadena textual empiezan por @ y también se incluyen entre comillas dobles. Por ejemplo:
La ventaja de las cadenas textuales es que las secuencias de escape no se procesan, lo que facilita la escritura de,
por ejemplo, un nombre de archivo Windows completo:
Tipo delegate
La declaración de un tipo delegado es similar a una firma de método. Tiene un valor devuelto y un número
cualquiera de parámetros de cualquier tipo:
En. NET, los tipos System.Action y System.Func proporcionan definiciones genéricas para muchos delegados
comunes. Es probable que no sea necesario definir nuevos tipos delegados personalizados. En su lugar, puede
crear instancias de los tipos genéricos proporcionados.
Un delegate es un tipo de referencia que puede usarse para encapsular un método con nombre o anónimo.
Los delegados son similares a los punteros de función en C++; pero son más seguros y proporcionan mayor
seguridad de tipos. Para las aplicaciones de delegados, vea Delegados y Delegados genéricos. Los delegados son
la base de los eventos. Se pueden crear instancias de un delegado asociándolo a un método con nombre o
anónimo.
Para crear instancias del delegado debe usarse un método o una expresión lambda que tenga un tipo de valor
devuelto y parámetros de entrada compatibles. Para obtener más información sobre el grado de variación
permitida en la firma de método, vea Varianza en delegados. Para el uso con métodos anónimos, el delegado y
el código que se van a asociar se declaran juntos.
Se produce un error en la combinación y eliminación de delegados con una excepción en tiempo de ejecución
cuando los tipos delegados implicados en el tiempo de ejecución son diferentes debido a la conversión de
variantes. En el ejemplo siguiente se muestra una situación que produce un error:
Puede crear un delegado con el tipo de tiempo de ejecución correcto mediante la creación de un objeto
delegado. En el ejemplo siguiente se muestra cómo se puede aplicar esta solución alternativa al ejemplo
anterior.
A partir de C# 9, puede declarar punteros de función, que usan una sintaxis similar. Un puntero de función usa la
instrucción calli en lugar de crear instancias de un tipo delegado y llamar al método virtual Invoke .
Tipo dynamic
El tipo dynamic indica el uso de la variable y las referencias a su comprobación de tipos en el tiempo de
compilación de omisión de miembros. En su lugar, se resuelven estas operaciones en tiempo de ejecución. El
tipo dynamic simplifica el acceso a las API de COM como las API de automatización de Office, a API dinámicas
como las bibliotecas de IronPython, y a Document Object Model (DOM) HTML.
El tipo dynamic se comporta como el tipo object en la mayoría de las circunstancias. En concreto, se puede
convertir cualquier expresión no NULL para el tipo dynamic . El tipo dynamic se diferencia de object en que el
compilador no resuelve o no comprueba el tipo de las operaciones que contienen expresiones de tipo dynamic .
El compilador empaqueta información sobre la operación y esa información se usa después para evaluar la
operación en tiempo de ejecución. Como parte del proceso, las variables de tipo dynamic están compiladas en
las variables de tipo object . Por consiguiente, el tipo dynamic solo existe en tiempo de compilación, no en
tiempo de ejecución.
En el siguiente ejemplo se contrasta una variable de tipo dynamic con una variable de tipo object . Para
comprobar el tipo de cada variable en tiempo de compilación, coloque el puntero del mouse sobre dyn u obj
en las instrucciones WriteLine . Copie el código siguiente en un editor donde IntelliSense esté disponible.
IntelliSense muestra dynamic para dyn y object para obj .
class Program
{
static void Main(string[] args)
{
dynamic dyn = 1;
object obj = 1;
// Rest the mouse pointer over dyn and obj to see their
// types at compile time.
System.Console.WriteLine(dyn.GetType());
System.Console.WriteLine(obj.GetType());
}
}
Las instrucciones WriteLine muestran los tipos en tiempo de ejecución de dyn y obj . En ese punto, ambos
tienen el mismo tipo, entero. Se produce el siguiente resultado:
System.Int32
System.Int32
Para ver la diferencia entre dyn y obj en tiempo de compilación, agregue las dos líneas siguientes entre las
declaraciones y las instrucciones WriteLine en el ejemplo anterior.
dyn = dyn + 3;
obj = obj + 3;
Un error del compilador se notifica para el intento de suma de un entero y un objeto en la expresión obj + 3 .
En cambio, no se notifica ningún error para dyn + 3 . En tiempo de compilación no se comprueba la expresión
que contiene dyn porque el tipo de dyn es dynamic .
El ejemplo siguiente usa dynamic en varias declaraciones. El método Main también contrasta la comprobación
de tipo en tiempo de compilación con la comprobación de tipo en tiempo de ejecución.
using System;
namespace DynamicExamples
{
class Program
{
static void Main(string[] args)
{
ExampleClass ec = new ExampleClass();
Console.WriteLine(ec.exampleMethod(10));
Console.WriteLine(ec.exampleMethod("value"));
class ExampleClass
{
static dynamic field;
dynamic prop { get; set; }
if (d is int)
{
return local;
}
else
{
return two;
}
}
}
}
// Results:
// Local variable
// 2
// Local variable
Vea también
Referencia de C#
Palabras clave de C#
Eventos
Uso de tipo dinámico
Procedimientos recomendados para el uso de cadenas
Operaciones básicas de cadenas
Creación de cadenas
Operadores de conversión y prueba de tipos
Procedimiento para convertir de forma segura mediante la coincidencia de patrones y los operadores is y as
Tutorial: Crear y usar objetos dinámicos (C# y Visual Basic)
System.Object
System.String
System.Dynamic.DynamicObject
Registros (referencia de C#)
16/09/2021 • 16 minutes to read
A partir de C# 9, se usa la palabra clave record para definir un tipo de referencia que proporciona
funcionalidad integrada para encapsular los datos. Puede crear tipos de registros con propiedades inmutables
mediante parámetros posicionales o sintaxis de propiedades estándar:
Aunque los registros pueden ser mutables, están destinados principalmente a admitir modelos de datos
inmutables. El tipo de registro ofrece las siguientes características:
Sintaxis concisa para crear un tipo de referencia con propiedades inmutables
Comportamiento integrado útil para un tipo de referencia centrado en datos:
Igualdad de valores
Sintaxis concisa para la mutación no destructiva
Formato integrado para la presentación
Compatibilidad con las jerarquías de herencia
También puede utilizar tipos de estructura para diseñar tipos centrados en datos que proporcionen igualdad de
valores y un comportamiento escaso o inexistente. Pero, en el caso de los modelos de datos relativamente
grandes, los tipos de estructura tienen algunas desventajas:
No admiten la herencia.
Son menos eficaces a la hora de determinar la igualdad de valores. En el caso de los tipos de valor, el método
ValueType.Equals usa la reflexión para buscar todos los campos. En el caso de los registros, el compilador
genera el método Equals . En la práctica, la implementación de la igualdad de valores en los registros es
bastante más rápida.
Usan más memoria en algunos escenarios, ya que cada instancia tiene una copia completa de todos los
datos. Los tipos de registro son tipos de referencia, por lo que una instancia de registro solo contiene una
referencia a los datos.
Cuando se usa la sintaxis posicional para la definición de propiedad, el compilador crea lo siguiente:
Una propiedad pública implementada automáticamente de solo inicialización para cada parámetro
posicional proporcionado en la declaración de registro. Una propiedad de solo inicialización solo se puede
establecer en el constructor o mediante un inicializador de propiedad.
Un constructor primario cuyos parámetros coinciden con los parámetros posicionales en la declaración del
registro.
Un método Deconstruct con un parámetro out para cada parámetro posicional proporcionado en la
declaración de registro. Este método solo se proporciona si hay dos o más parámetros posicionales. El
método deconstruye las propiedades definidas mediante la sintaxis posicional; omite las propiedades que se
definen mediante la sintaxis de propiedades estándar.
Es posible que le interese agregar atributos a cualquiera de estos elementos que el compilador crea a partir de
la definición de registro. Puede agregar un destino a cualquier atributo que aplique a las propiedades del
registro posicional. En el ejemplo siguiente se aplica System.Text.Json.Serialization.JsonPropertyNameAttribute a
cada propiedad del registro Person . El destino property: indica que el atributo se aplica a la propiedad
generada por el compilador. Otros valores son field: para aplicar el atributo al campo y param: para aplicar el
atributo al parámetro.
/// <summary>
/// Person record type
/// </summary>
/// <param name="FirstName">First Name</param>
/// <param name="LastName">Last Name</param>
/// <remarks>
/// The person type is a positional record containing the
/// properties for the first and last name. Those properties
/// map to the JSON elements "firstName" and "lastName" when
/// serialized or deserialized.
/// </remarks>
public record Person([property: JsonPropertyName("firstName")]string FirstName,
[property: JsonPropertyName("lastName")]string LastName);
En el ejemplo anterior también se muestra cómo crear comentarios de documentación XML para el registro.
Puede agregar la etiqueta <param> para agregar documentación para los parámetros del constructor principal.
Si la definición de propiedad implementada automáticamente generada no es la que desea, puede definir su
propia propiedad con el mismo nombre. Si lo hace, el constructor y el deconstructor generados usarán su
definición de propiedad. Por ejemplo, en el ejemplo siguiente se crea la propiedad posicional FirstName
internal en lugar de public .
public record Person(string FirstName, string LastName)
{
internal string FirstName { get; init; } = FirstName;
}
Un tipo de registro no tiene que declarar ninguna propiedad posicional. Puede declarar un registro sin
propiedades posicionales y también campos y propiedades adicionales, como en el ejemplo siguiente:
Si define propiedades mediante la sintaxis de propiedades estándar, pero omite el modificador de acceso, las
propiedades son private implícitamente.
Inmutabilidad
Un tipo de registro no es necesariamente inmutable. Puede declarar propiedades con descriptores de acceso
set y campos que no sean readonly . Sin embargo, aunque los registros pueden ser mutables, facilitan la
creación de modelos de datos inmutables.
La inmutabilidad puede resultar útil si necesita que un tipo centrado en datos sea seguro para subprocesos o si
depende de que un código hash quede igual en una tabla hash. Sin embargo, la inmutabilidad no es adecuada
para todos los escenarios de datos. Por ejemplo, Entity Framework Core no admite la actualización con tipos de
entidad inmutables.
Las propiedades de solo inicialización, tanto si se crean a partir de parámetros posicionales como al especificar
descriptores de acceso init , tienen una inmutabilidad superficial. Después de la inicialización, no se puede
cambiar el valor de las propiedades de tipo de valor ni la referencia de las propiedades de tipo de referencia. Sin
embargo, se pueden cambiar los datos a los que hace referencia una propiedad de tipo de referencia. En el
ejemplo siguiente se muestra que el contenido de una propiedad inmutable de tipo de referencia (una matriz en
este caso) es mutable:
person.PhoneNumbers[0] = "555-6789";
Console.WriteLine(person.PhoneNumbers[0]); // output: 555-6789
}
Las características exclusivas de los tipos de registro se implementan mediante métodos sintetizados por el
compilador, y ninguno de estos métodos pone en peligro la inmutabilidad mediante la modificación del estado
del objeto.
Igualdad de valores
La igualdad de valores significa que dos variables de un tipo de registro son iguales si los tipos coinciden y
todos los valores de propiedad y campo coinciden. Para otros tipos de referencia, la igualdad significa identidad.
Es decir, dos variables de un tipo de referencia son iguales si hacen referencia al mismo objeto.
Se requiere la igualdad de referencia en algunos modelos de datos. Por ejemplo, Entity Framework Core
depende de la igualdad de referencia para garantizar que solo usa una instancia de un tipo de entidad para lo
que es conceptualmente una entidad. Por esta razón, los tipos de registro no son adecuados para su uso como
tipos de entidad en Entity Framework Core.
En el ejemplo siguiente se muestra la igualdad de valores de tipos de registro:
person1.PhoneNumbers[0] = "555-1234";
Console.WriteLine(person1 == person2); // output: True
En los tipos class , podría invalidar manualmente los métodos y los operadores de igualdad para lograr la
igualdad de valores, pero el desarrollo y las pruebas de ese código serían lentos y propensos a errores. Al tener
esta funcionalidad integrada, se evitan los errores que resultarían de olvidarse de actualizar el código de
invalidación personalizado cuando se agreguen o cambien propiedades o campos.
Puede escribir sus propias implementaciones para reemplazar cualquiera de estos métodos sintetizados. Si un
tipo de registro tiene un método que coincide con la signatura de cualquier método sintetizado, el compilador
no sintetiza ese método.
Si proporciona su propia implementación de Equals en un tipo de registro, proporcione también una
implementación de GetHashCode .
Mutación no destructiva
Si necesita mutar propiedades inmutables de una instancia de registro, puede usar una expresión with para
lograr una mutación no destructiva. Una expresión with crea una instancia de registro que es una copia de una
instancia de registro existente, con las propiedades y los campos especificados modificados. Use la sintaxis del
inicializador de objeto para especificar los valores que se van a cambiar, como se muestra en el ejemplo
siguiente:
public record Person(string FirstName, string LastName)
{
public string[] PhoneNumbers { get; init; }
}
La expresión with puede establecer propiedades posicionales o propiedades creadas con la sintaxis de
propiedades estándar. Las propiedades no posicionales deben tener un descriptor de acceso init o set para
cambiar en una expresión with .
El resultado de una expresión with es una copia superficial, lo que significa que, para una propiedad de
referencia, solo se copia la referencia a una instancia. Tanto el registro original como la copia terminan con una
referencia a la misma instancia.
Para implementar esta característica, el compilador sintetiza un método de clon y un constructor de copias. El
método de clonación virtual devuelve un nuevo registro inicializado por el constructor de copia. Cuando se usa
una expresión with , el compilador crea código que llama al método de clonación y, después, establece las
propiedades que se especifican en la expresión with .
Si necesita un comportamiento de copia diferente, puede escribir su propio constructor de copia. Si lo hace, el
compilador no sintetizará un método. Cree su constructor private si el registro es sealed ; de lo contrario,
conviértalo en protected .
No puede invalidar el método de clon y no puede crear un miembro denominado Clone . El nombre real del
método de clon lo genera el compilador.
<record type name> { <property name> = <value>, <property name> = <value>, ...}
En el caso de los tipos de referencia, se muestra el nombre del tipo del objeto al que hace referencia la
propiedad en lugar del valor de propiedad. En el ejemplo siguiente, la matriz es un tipo de referencia, por lo que
se muestra System.String[] en lugar de los valores de los elementos de matriz reales:
Person { FirstName = Nancy, LastName = Davolio, ChildNames = System.String[] }
Para implementar esta característica, el compilador sintetiza un método PrintMembers virtual y una invalidación
ToString. La invalidación ToString crea un objeto StringBuilder con el nombre de tipo seguido de un corchete
de apertura. Llama a PrintMembers para agregar nombres y valores de propiedad y, a continuación, agrega el
corchete de cierre. En el ejemplo siguiente se muestra código similar al que contiene la invalidación sintetizada:
Herencia
Un registro puede heredar de otro registro. Sin embargo, un registro no puede heredar de una clase, y una clase
no puede heredar de un registro.
Parámetros posicionales en tipos de registro derivados
El registro derivado declara parámetros para todos los parámetros del constructor primario del registro base. El
registro base declara e inicializa esas propiedades. El registro derivado no las oculta, sino que solo crea e
inicializa propiedades para los parámetros que no se han declarado en su registro base.
En el ejemplo siguiente se muestra la herencia con la sintaxis de la propiedad posicional:
En el ejemplo, todas las instancias tienen las mismas propiedades y los mismos valores de propiedad. Pero
student == teacher devuelve False , aunque ambas son variables de tipo Person , y student == student2
devuelve True , aunque una es una variable Person y otra es una variable Student .
Para implementar este comportamiento, el compilador sintetiza una propiedad EqualityContract que devuelve
un objeto Type que coincide con el tipo del registro. Esto permite a los métodos de igualdad comparar el tipo en
tiempo de ejecución de los objetos cuando están comprobando la igualdad. Si el tipo base de un registro es
object , esta propiedad es virtual . Si el tipo base es otro tipo de registro, la propiedad es una invalidación. Si
el tipo de registro es sealed , esta propiedad es sealed .
Al comparar dos instancias de un tipo derivado, los métodos de igualdad sintetizados comprueban la igualdad
de todas las propiedades de los tipos base y derivados. El método GetHashCode sintetizado usa el método
GetHashCode de todas las propiedades y los campos declarados en el tipo base y el tipo de registro derivado.
Puede proporcionar su propia implementación del método PrintMembers . Si lo hace, use la siguiente firma:
Para un registro sealed que deriva de (no declara un registro base):
object
private bool PrintMembers(StringBuilder builder) .
Para un registro sealed que deriva de otro registro:
protected sealed override bool PrintMembers(StringBuilder builder) .
Para un registro que no es sealed y que deriva del objeto:
protected virtual bool PrintMembers(StringBuilder builder); .
Para un registro que no es sealed y que deriva de otro registro:
protected override bool PrintMembers(StringBuilder builder); .
Este es un ejemplo de código que reemplaza los métodos sintetizados PrintMembers , uno para un tipo de
registro que deriva de un objeto y otro para un tipo de registro que deriva de otro registro:
public abstract record Person(string FirstName, string LastName, string[] PhoneNumbers)
{
protected virtual bool PrintMembers(StringBuilder stringBuilder)
{
stringBuilder.Append($"FirstName = {FirstName}, LastName = {LastName}, ");
stringBuilder.Append($"PhoneNumber1 = {PhoneNumbers[0]}, PhoneNumber2 = {PhoneNumbers[1]}");
return true;
}
}
public record Teacher(string FirstName, string LastName, string[] PhoneNumbers, int Grade)
: Person(FirstName, LastName, PhoneNumbers)
{
protected override bool PrintMembers(StringBuilder stringBuilder)
{
if (base.PrintMembers(stringBuilder))
{
stringBuilder.Append(", ");
};
stringBuilder.Append($"Grade = {Grade}");
return true;
}
};
NOTE
En C# 10.0 y versiones posteriores, el compilador sintetizará PrintMembers cuando un registro base haya sellado el
método ToString . También puede crear una implementación de PrintMembers propia.
Vea también
Referencia de C#
Instrucciones de diseño: elección entre clase y estructura
Instrucciones de diseño: diseño de estructuras
Clases, estructuras y registros
Expresión with
class (Referencia de C#)
16/09/2021 • 2 minutes to read
Las clases se declaran mediante la palabra clave class , como se muestra en el siguiente ejemplo:
class TestClass
{
// Methods, properties, fields, events, delegates
// and nested classes go here.
}
Comentarios
Solo la herencia simple se permite en C#. En otras palabras, una clase puede heredar la implementación solo de
una clase base. En cambio, una clase puede implementar más de una interfaz. En la tabla siguiente se muestran
ejemplos de herencia de clases e implementación de interfaces:
H EREN C IA E JEM P LO
Las clases que se declaran directamente dentro de un espacio de nombres, que no están anidadas dentro de
otras clases, pueden ser de tipo public o internal. De forma predeterminada, las clases son internal .
Los miembros de clase, incluidas las clases anidadas, pueden ser public, protected internal, protected, internal,
private o private protected. De forma predeterminada, los miembros son private .
Para obtener más información, consulte Modificadores de acceso.
Puede declarar clases genéricas que tengan parámetros de tipo. Para obtener más información, consulte Clases
genéricas.
Una clase puede contener declaraciones de los miembros siguientes:
Constructores
Constantes
Fields
Finalizadores
Métodos
Propiedades
Indizadores
Operadores
Eventos
Delegados
Clases
Interfaces
Tipos de estructura
Tipos de enumeración
Ejemplo
En el ejemplo siguiente se muestra cómo declarar campos de clase, constructores y métodos. También se
muestra la creación de instancias de objeto y la impresión de datos de instancias. En este ejemplo, se declaran
dos clases. La primera clase, Child , contiene dos campos privados ( name y age ), dos constructores públicos y
un método público. La segunda clase, StringTest , se usa para contener Main .
class Child
{
private int age;
private string name;
// Default constructor:
public Child()
{
name = "N/A";
}
// Constructor:
public Child(string name, int age)
{
this.name = name;
this.age = age;
}
// Printing method:
public void PrintChild()
{
Console.WriteLine("{0}, {1} years old.", name, age);
}
}
class StringTest
{
static void Main()
{
// Create objects by using the new operator:
Child child1 = new Child("Craig", 11);
Child child2 = new Child("Sally", 10);
// Display results:
Console.Write("Child #1: ");
child1.PrintChild();
Console.Write("Child #2: ");
child2.PrintChild();
Console.Write("Child #3: ");
child3.PrintChild();
}
}
/* Output:
Child #1: Craig, 11 years old.
Child #2: Sally, 10 years old.
Child #3: N/A, 0 years old.
*/
Comentarios
Observe que en el ejemplo anterior solo se puede acceder a los campos privados ( name y age ) a través del
método público de la clase Child . Por ejemplo, no puede imprimir el nombre de la clase child, desde el método
Main , mediante una instrucción como esta:
Console.Write(child1.name); // Error
El acceso a miembros privados de Child desde Main solo sería posible si Main fuera un miembro de la clase.
Los tipos declarados dentro de una clase sin un modificador de acceso adoptan el valor predeterminado de
private , por lo que los miembros de datos de este ejemplo seguirían siendo private si se quitara la palabra
clave.
Por último, tenga en cuenta que, para el objeto creado mediante el constructor sin parámetros ( child3 ), el
campo age se ha inicializado en cero de forma predeterminada.
Consulte también
Referencia de C#
Guía de programación de C#
Palabras clave de C#
Tipos de referencia
interface (Referencia de C#)
16/09/2021 • 3 minutes to read
Una interfaz define un contrato. Cualquier class o struct que implemente ese contrato debe proporcionar
una implementación de los miembros definidos en la interfaz. A partir de C# 8.0, una interfaz puede definir una
implementación predeterminada de miembros. También puede definir miembros static para proporcionar
una única implementación de funcionalidad común.
En el ejemplo siguiente, la clase ImplementationClass debe implementar un método denominado SampleMethod
que no tiene ningún parámetro y devuelve void .
Para obtener más información y ejemplos, vea Interfaces.
Interfaz de ejemplo
interface ISampleInterface
{
void SampleMethod();
}
Una interfaz puede ser un miembro de un espacio de nombres o una clase. Una declaración de interfaz puede
contener declaraciones (firmas sin ninguna implementación) de los miembros siguientes:
Métodos
Propiedades
Indizadores
Eventos
Normalmente, estas declaraciones de miembros anteriores no contienen ningún cuerpo. A partir de C# 8.0, un
miembro de interfaz puede declarar un cuerpo. Esto se conoce como implementación predeterminada. Los
miembros con cuerpos permiten que la interfaz proporcione una implementación "predeterminada" de las
clases y las estructuras que no proporcionan una implementación de invalidación. Además, a partir de C# 8.0,
una interfaz puede incluir:
Constantes
Operadores
Constructor estático.
Tipos anidados
Campos, métodos, propiedades, indizadores y eventos estáticos.
Declaraciones de miembros con la sintaxis de implementación de interfaz explícita.
Modificadores de acceso explícitos (el acceso predeterminado es public ).
Es posible que las interfaces no contengan estado de instancia. Aunque los campos estáticos ahora están
permitidos, los campos de instancia no se permiten en las interfaces. Las propiedades automáticas de instancia
no se admiten en las interfaces, ya que declararían de forma implícita un campo oculto. Esta regla tiene un
efecto sutil en las declaraciones de propiedad. En una declaración de interfaz, el código siguiente no declara una
propiedad implementada automáticamente como hace en una class o un struct . En su lugar, declara una
propiedad que no tiene una implementación predeterminada pero que se debe implementar en cualquier tipo
que implemente la interfaz:
Una interfaz puede heredar de una o varias interfaces base. Cuando una interfaz invalida un método
implementado en una interfaz base, debe usar la sintaxis de implementación de interfaz explícita.
Cuando una lista de tipos base contiene una clase e interfaces base, la clase base debe aparecer primero en la
lista.
Una clase que implementa una interfaz puede implementar explícitamente miembros de esa interfaz. A un
miembro implementado explícitamente solo se puede tener acceso mediante una instancia de la interfaz, y no
mediante una instancia de la clase. Además, solo se puede acceder a los miembros de interfaz predeterminados
a través de una instancia de la interfaz.
Para obtener más información sobre la implementación de interfaz explícita, vea Implementación de interfaz
explícita.
int Y
{
get;
set;
}
double Distance
{
get;
}
}
// Property implementation:
public int X { get; set; }
// Property implementation
public double Distance =>
Math.Sqrt(X * X + Y * Y);
class MainClass
{
static void PrintPoint(IPoint p)
{
Console.WriteLine("x={0}, y={1}", p.X, p.Y);
}
Vea también
Referencia de C#
Guía de programación de C#
Palabras clave de C#
Tipos de referencia
Interfaces
Utilizar propiedades
Utilizar indizadores
Tipos de referencia que aceptan valores NULL
(referencia de C#)
16/09/2021 • 6 minutes to read
NOTE
En este artículo se tratan los tipos de referencia que aceptan valores NULL. También puede declarar tipos de valor que
aceptan valores NULL.
Los tipos de referencia que aceptan valores NULL están disponibles a partir de C# 8.0, en un código que ha
participado en un contexto compatible con valores NULL. Los tipos de referencia que aceptan valores NULL, las
advertencias de análisis estático NULL y el operador null-forgiving son características de lenguaje opcionales.
Todas están desactivadas de forma predeterminada. Un contexto que admite valores NULL se controla en el
nivel de proyecto mediante la configuración de compilación o en el código que usa pragmas.
En un contexto compatible con valores NULL:
Una variable de un tipo de referencia T se debe inicializar con un valor distinto de NULL y nunca puede
tener asignado un valor que sea null .
Una variable de un tipo de referencia T? se puede inicializar con null o asignarle null , pero debe
comprobarse con null antes de desreferenciarla.
Una variable m de tipo T? se considera que no es NULL cuando se aplica el operador null-forgiving, como
en m! .
Las distinciones entre un tipo de referencia que no acepta valores NULL T y un tipo de referencia que acepta
valores NULL T? se aplican mediante la interpretación del compilador de las reglas anteriores. Una variable de
tipo T y una variable de tipo T? se representan mediante el mismo tipo .NET. En el ejemplo siguiente se
declara una cadena que no acepta valores NULL y una cadena que acepta valores NULL y luego se usa el
operador null-forgiving para asignar un valor a una cadena que no acepta valores NULL:
Las variables notNull y nullable se ambas representan con el tipo String. Dado que los tipos que no aceptan
valores NULL y que aceptan valores NULL se almacenan como el mismo tipo, hay varias ubicaciones en las que
no se permite el uso de un tipo de referencia que acepte valores NULL. En general, un tipo de referencia que
acepta valores NULL no se puede usar como clase base o interfaz implementada. No se puede usar un tipo de
referencia que acepte valores NULL en ninguna expresión de creación de objetos o de expresión de prueba de
tipos. Un tipo de referencia que acepta valores NULL no puede ser el tipo de una expresión de acceso a
miembros. En los siguientes ejemplos se muestran estas construcciones:
public MyClass : System.Object? // not allowed
{
}
En el fragmento de código siguiente se muestra dónde el compilador emite advertencias al utilizar esta clase:
En los ejemplos anteriores se muestra el análisis estático del compilador para determinar el estado NULL de las
variables de referencia. El compilador aplica las reglas de lenguaje a las asignaciones y comprobaciones de
valores NULL para informar de su análisis. El compilador no puede hacer suposiciones sobre la semántica de
métodos o propiedades. Si llama a métodos que realizan comprobaciones de valores NULL, el compilador no
puede saber que estos métodos afectan al estado NULL de una variable. Hay una serie de atributos que puede
agregar a las API para informar al compilador sobre la semántica de los argumentos y los valores devueltos.
Estos atributos se han aplicado a muchas API comunes de las bibliotecas de .NET Core. Por ejemplo,
IsNullOrEmpty se ha actualizado y el compilador interpreta correctamente ese método como una comprobación
de valores NULL. Para obtener más información sobre los atributos que se aplican al análisis estático de estado
NULL, consulte el artículo sobre atributos que aceptan valores NULL.
Vea también
Referencia de C#
Tipos de valores que aceptan valores NULL
void (referencia de C#)
16/09/2021 • 2 minutes to read
Use void como el tipo de valor devuelto de un método (o una función local) para especificar que el método no
devuelve un valor.
También puede usar void como un tipo de referente para declarar un puntero a un tipo desconocido. Para
obtener más información, vea Tipos de puntero.
No se puede usar void como el tipo de una variable.
Vea también
Referencia de C#
System.Void
var (Referencia de C#)
16/09/2021 • 2 minutes to read
A partir de C# 3, las variables que se declaran en el ámbito de método pueden tener un "tipo" var implícito.
Una variable local con tipo implícito es fuertemente tipada exactamente igual que si hubiera declarado el tipo,
solo que en este caso es el compilador el que lo determina. Las dos declaraciones siguientes de i tienen una
función equivalente:
IMPORTANT
Cuando var se usa con tipos de referencia que aceptan valores NULL, siempre implica un tipo de referencia que acepta
valores NULL, aunque el tipo de expresión no los acepte.
Un uso común de la palabra clave var es con las expresiones de invocación del constructor. El uso de var
permite no repetir un nombre de tipo en una declaración de variable y una creación de instancias de objeto,
como se muestra en el ejemplo siguiente:
A partir de C# 9.0, se puede usar una expresión new de con tipo de destino como alternativa:
List<int> xs = new();
List<int>? ys = new();
Ejemplo
En el siguiente ejemplo se muestran dos expresiones de consulta. En la primera expresión, el uso de var se
permite pero no es necesario, ya que el tipo del resultado de la consulta se puede indicar explícitamente como
IEnumerable<string> . En cambio, en la segunda expresión, var permite que el resultado sea una colección de
tipos anónimos y solo el compilador puede tener acceso al nombre de ese tipo. El uso de var elimina la
necesidad de crear una nueva clase para el resultado. Tenga en cuenta que, en el ejemplo 2, la variable de
iteración foreach``item también debe tener tipo implícito.
// Example #1: var is optional when
// the select clause specifies a string
string[] words = { "apple", "strawberry", "grape", "peach", "banana" };
var wordQuery = from word in words
where word[0] == 'g'
select word;
Vea también
Referencia de C#
Variables locales con asignación implícita de tipos
Relaciones entre tipos en las operaciones de consulta LINQ
Tipos integrados (referencia de C#)
16/09/2021 • 2 minutes to read
PA L A B RA C L AVE DE T IP O DE C # T IP O DE . N ET
bool System.Boolean
byte System.Byte
sbyte System.SByte
char System.Char
decimal System.Decimal
double System.Double
float System.Single
int System.Int32
uint System.UInt32
nint System.IntPtr
nuint System.UIntPtr
long System.Int64
ulong System.UInt64
short System.Int16
ushort System.UInt16
PA L A B RA C L AVE DE T IP O DE C # T IP O DE . N ET
object System.Object
string System.String
dynamic System.Object
En las tablas anteriores, cada palabra clave de tipo de C# de la columna ubicada a la izquierda (excepto nint y
nuint y dynamic) es un alias del tipo de .NET correspondiente. Son intercambiables. Por ejemplo, en las
declaraciones siguientes se declaran variables del mismo tipo:
int a = 123;
System.Int32 b = 123;
Los tipos nint y nuint son enteros de tamaño nativo. Se representan internamente por los tipos de .NET
indicados, pero, en cada caso, la palabra clave y el tipo de .NET no son intercambiables. El compilador
proporciona operaciones y conversiones para nint y nuint como tipos enteros que no proporciona para los
tipos de puntero System.IntPtr y System.UIntPtr . Para obtener más información, consulte los tipos nint y
nuint .
La palabra clave void representa la ausencia de un tipo. Se usa como el tipo de valor devuelto de un método
que no devuelve un valor.
Vea también
Referencia de C#
Valores predeterminados de los tipos de C#
Tipos no administrados (referencia de C#)
16/09/2021 • 2 minutes to read
using System;
Un struct genérico puede ser el origen de los tipos no administrados y de los tipos construidos no
administrados. En el ejemplo anterior se define un struct Coords<T> genérico y se presentan los ejemplos de
tipos construidos no administrados. El ejemplo de un tipo no administrado es Coords<object> . No está
administrado porque tiene los campos del tipo object , que es no administrado. Si desea que todos los tipos
construidos sean tipos no administrados, use la restricción unmanaged en la definición de un struct genérico:
Vea también
Referencia de C#
Tipos de puntero
Tipos relacionados con el intervalo y la memoria
sizeof (operador)
stackalloc
Valores predeterminados de los tipos de C#
(referencia de C#)
16/09/2021 • 2 minutes to read
T IP O VA LO R P REDET ERM IN A DO
bool false
Cualquier tipo de valor que acepta valores NULL Instancia para la que la propiedad HasValue es false y la
propiedad Value no está definida. Este valor predeterminado
también se conoce con el valor null de un tipo de valor que
acepta valores NULL.
Use el operador default para producir el valor predeterminado de un tipo, como se muestra en el ejemplo
siguiente:
int a = default(int);
A partir de C# 7.1, se puede usar el literal default para inicializar una variable con el valor predeterminado de
su tipo:
int a = default;
Para un tipo de valor, el constructor implícito sin parámetros también genera el valor predeterminado del tipo,
como se muestra en el ejemplo siguiente:
En tiempo de ejecución, si la instancia de System.Type representa un tipo de valor, puede usar el método
Activator.CreateInstance(Type) para invocar el constructor sin parámetros y obtener el valor predeterminado del
tipo.
Vea también
Referencia de C#
Constructores
Palabras clave de C#
16/09/2021 • 2 minutes to read
Las palabras clave son identificadores reservados predefinidos que tienen un significado especial para el
compilador. No podrá utilizarlos como identificadores en el programa a no ser que incluyan @ como prefijo.
Por ejemplo, @if es un identificador válido, pero if no lo es, porque if es una palabra clave.
En la primera tabla de este tema se muestran las palabras clave que son identificadores reservados en cualquier
parte de un programa en C#. En la segunda tabla de este tema se enumeran las palabras clave contextuales en
C#. Las palabras clave contextuales tienen un significado especial solo en un contexto de programa limitado y
pueden utilizarse como identificadores fuera de ese contexto. Por lo general, cuando se agregan nuevas palabras
clave al lenguaje C#, se agregan como palabras clave contextuales para evitar la interrupción de los programas
escritos en versiones anteriores.
abstract
as
base
bool
break
byte
case
catch
char
checked
class
const
continue
decimal
default
delegate
do
double
else
enum
event
explicit
extern
false
finally
fixed
float
for
foreach
goto
if
implicit
in
int
interface
internal
is
lock
long
namespace
new
null
object
operator
out
override
params
private
protected
public
readonly
ref
return
sbyte
sealed
short
sizeof
stackalloc
static
string
struct
switch
this
throw
true
try
typeof
uint
ulong
unchecked
unsafe
ushort
using
virtual
void
volatile
while
Vea también
Referencia de C#
Modificadores de acceso (Referencia de C#)
16/09/2021 • 2 minutes to read
Los modificadores de acceso son palabras clave que se usan para especificar la accesibilidad declarada de un
miembro o un tipo. En esta sección se presentan los cuatro modificadores de acceso:
public
protected
internal
private
Pueden especificarse los siguientes seis niveles de accesibilidad con los modificadores de acceso:
public : El acceso no está restringido.
protected : El acceso está limitado a la clase contenedora o a los tipos derivados de la clase contenedora.
internal : El acceso está limitado al ensamblado actual.
protected internal : El acceso está limitado al ensamblado actual o a los tipos derivados de la clase
contenedora.
private : El acceso está limitado al tipo contenedor.
private protected : El acceso está limitado a la clase contenedora o a los tipos derivados de la clase
contenedora que hay en el ensamblado actual.
En esta sección también se presenta lo siguiente:
Niveles de accesibilidad: usar los cuatro modificadores de acceso para declarar seis niveles de
accesibilidad.
Dominio de accesibilidad: especifica en qué secciones del programa se puede hacer referencia a dicho
miembro.
Restricciones en el uso de niveles de accesibilidad: un resumen de las restricciones sobre usar niveles de
accesibilidad declarados.
Vea también
Referencia de C#
Guía de programación de C#
Palabras clave de C#
Modificadores de acceso
Palabras clave de acceso
Modificadores
Niveles de accesibilidad (Referencia de C#)
16/09/2021 • 2 minutes to read
Use los modificadores de acceso public , protected , internal o private para especificar uno de los
siguientes niveles de accesibilidad declarada para miembros.
Solo se permite un modificador de acceso para un miembro o tipo, excepto cuando se usan las combinaciones
protected internal o private protected .
protected
internal
private
protected internal
private protected
protected
internal
private *
protected internal
private protected
internal
private
* Un miembro interface con accesibilidad private debe tener una implementación predeterminada.
La accesibilidad de un tipo anidado depende de su dominio de accesibilidad, que viene determinado por la
accesibilidad declarada del miembro y el dominio de accesibilidad del tipo contenedor inmediato. Sin embargo,
el dominio de accesibilidad de un tipo anidado no puede superar al del tipo contenedor.
Vea también
Referencia de C#
Guía de programación de C#
Palabras clave de C#
Modificadores de acceso
Dominio de accesibilidad
Restricciones en el uso de los niveles de accesibilidad
Modificadores de acceso
public
private
protected
internal
Dominio de accesibilidad (Referencia de C#)
16/09/2021 • 2 minutes to read
El dominio de accesibilidad de un miembro especifica en qué secciones del programa se puede hacer referencia
a dicho miembro. Si el miembro está anidado dentro de otro tipo, su dominio de accesibilidad viene
determinado por el nivel de accesibilidad del miembro y por el dominio de accesibilidad del tipo contenedor
inmediato.
El dominio de accesibilidad de un tipo de nivel superior es por lo menos el texto del programa del proyecto en el
que se declara. Es decir, el dominio incluye todos los archivos de origen de este proyecto. El dominio de
accesibilidad de un tipo anidado es, al menos, el texto del programa del tipo en el que se declara. Es decir, el
dominio es el cuerpo del tipo, que incluye todos los tipos anidados. El dominio de accesibilidad de un tipo
anidado no puede superar nunca al del tipo contenedor. Estos conceptos se muestran en el siguiente ejemplo.
Ejemplo
Este ejemplo contiene un tipo de nivel superior, T1 , y dos clases anidadas, M1 y M2 . Las clases contienen
campos que tienen diferentes accesibilidades declaradas. En el método Main , a cada instrucción le sigue un
comentario que indica el dominio de accesibilidad de cada miembro. Observe que las instrucciones que intentan
hacer referencia a los miembros inaccesibles están marcadas con comentarios. Si quiere ver los errores
generados por el compilador cuando se intenta hacer referencia a un miembro inaccesible, quite los
comentarios de uno en uno.
public class T1
{
public static int publicInt;
internal static int internalInt;
private static int privateInt = 0;
static T1()
{
// T1 can access public or internal members
// in a public or private (or internal) nested class.
M1.publicInt = 1;
M1.internalInt = 2;
M2.publicInt = 3;
M2.internalInt = 4;
public class M1
{
public static int publicInt;
internal static int internalInt;
private static int privateInt = 0;
}
private class M2
{
public static int publicInt = 0;
internal static int internalInt = 0;
private static int privateInt = 0;
}
}
class MainClass
{
static void Main()
{
// Access is unlimited.
T1.publicInt = 1;
// Access is unlimited.
T1.M1.publicInt = 1;
Vea también
Referencia de C#
Guía de programación de C#
Palabras clave de C#
Modificadores de acceso
Niveles de accesibilidad
Restricciones en el uso de los niveles de accesibilidad
Modificadores de acceso
public
private
protected
internal
Restricciones en el uso de los niveles de
accesibilidad (Referencia de C#)
16/09/2021 • 3 minutes to read
Cuando especifique un tipo en una declaración, compruebe si el nivel de accesibilidad de este depende del nivel
de accesibilidad de un miembro o de otro tipo. Por ejemplo, la clase base directa debe ser al menos igual de
accesible que la clase derivada. Las siguientes declaraciones producen un error del compilador porque la clase
base BaseClass es menos accesible que MyClass :
C O N T EXTO C O M EN TA RIO S
Ejemplo
El siguiente ejemplo contiene declaraciones erróneas de diferentes tipos. El comentario que sigue a cada
declaración indica el error del compilador previsto.
// Restrictions on Using Accessibility Levels
// CS0052 expected as well as CS0053, CS0056, and CS0057
// To make the program work, change access level of both class B
// and MyPrivateMethod() to public.
using System;
// A delegate:
delegate int MyDelegate();
class B
{
// A private method:
static int MyPrivateMethod()
{
return 0;
}
}
public class A
{
// Error: The type B is less accessible than the field A.myField.
public B myField = new B();
public B MyMethod()
{
// Error: The type B is less accessible
// than the method A.MyMethod.
return new B();
}
Esta página trata sobre el modificador de acceso internal . La palabra clave internal también forma parte
del modificador de acceso protected internal .
Solo se puede tener acceso a los tipos internos o los miembros desde los archivos del mismo ensamblado,
como en este ejemplo:
Para obtener una comparación de internal con los demás modificadores de acceso, vea Niveles de
accesibilidad y Modificadores de acceso.
Para más información sobre los ensamblados, consulte Ensamblados en .NET.
Un uso común del acceso interno se da en el desarrollo basado en componentes porque permite que un grupo
de componentes cooperen de manera privada sin estar expuesto al resto del código de la aplicación. Por
ejemplo, un marco para crear interfaces gráficas de usuario podría proporcionar las clases Control y Form que
cooperan mediante miembros con acceso interno. Como estos miembros son internos, no se exponen al código
que usa el marco de trabajo.
Es un error hacer referencia a un tipo o miembro con acceso interno fuera del ensamblado en el que se definió.
Ejemplo 1
Este ejemplo contiene dos archivos, Assembly1.cs y Assembly1_a.cs . El primer archivo contiene una clase base
interna, BaseClass . En el segundo archivo, un intento de crear una instancia de BaseClass producirá un error.
// Assembly1.cs
// Compile with: /target:library
internal class BaseClass
{
public static int intM = 0;
}
// Assembly1_a.cs
// Compile with: /reference:Assembly1.dll
class TestAccess
{
static void Main()
{
var myBase = new BaseClass(); // CS0122
}
}
Ejemplo 2
En este ejemplo, use los mismos archivos usados en el ejemplo 1 y cambie el nivel de accesibilidad de
BaseClass a public . Cambie también el nivel de accesibilidad del miembro intM a internal . En este caso, se
puede crear una instancia de la clase, pero no se puede tener acceso al miembro interno.
// Assembly2.cs
// Compile with: /target:library
public class BaseClass
{
internal static int intM = 0;
}
// Assembly2_a.cs
// Compile with: /reference:Assembly2.dll
public class TestAccess
{
static void Main()
{
var myBase = new BaseClass(); // Ok.
BaseClass.intM = 444; // CS0117
}
}
Vea también
Referencia de C#
Guía de programación de C#
Palabras clave de C#
Modificadores de acceso
Niveles de accesibilidad
Modificadores
public
private
protected
private (Referencia de C#)
16/09/2021 • 2 minutes to read
Esta página trata sobre el modificador de acceso private . La palabra clave private también forma parte
del modificador de acceso private protected .
El acceso privado es el nivel de acceso menos permisivo. Los miembros privados solo son accesibles dentro del
cuerpo de la clase o el struct en el que se declaran, como en este ejemplo:
class Employee
{
private int i;
double d; // private access by default
}
Los tipos anidados en el mismo cuerpo también pueden tener acceso a los miembros privados.
Hacer referencia a un miembro privado fuera de la clase o el struct en el que se declara es un error en tiempo de
compilación.
Para obtener una comparación de private con los demás modificadores de acceso, vea Niveles de accesibilidad
y Modificadores de acceso.
Ejemplo
En este ejemplo, la clase Employee contiene dos miembros de datos privados, name y salary . Como miembros
privados, solo pueden tener acceso a ellos los métodos de miembro. Los métodos públicos denominados
GetName y Salary se agregan para permitir un acceso controlado a los miembros privados. Se tiene acceso al
miembro name a través de un método público, mientras que se tiene acceso al miembro salary a través de
una propiedad pública de solo lectura. (Para obtener más información, vea Propiedades).
class Employee2
{
private string name = "FirstName, LastName";
private double salary = 100.0;
class PrivateTest
{
static void Main()
{
var e = new Employee2();
Vea también
Referencia de C#
Guía de programación de C#
Palabras clave de C#
Modificadores de acceso
Niveles de accesibilidad
Modificadores
public
protected
internal
protected (Referencia de C#)
16/09/2021 • 2 minutes to read
NOTE
Esta página trata sobre el modificador de acceso protected . La palabra clave protected también forma parte de los
modificadores de acceso protected internal y private protected .
Un miembro protegido es accesible dentro de su clase y por parte de instancias de clase derivadas.
Para obtener una comparación de protected con los demás modificadores de acceso, vea Niveles de
accesibilidad.
Ejemplo 1
Un miembro protegido de una clase base es accesible en una clase derivada únicamente si el acceso se produce
a través del tipo de clase derivada. Por ejemplo, vea el siguiente segmento de código:
class A
{
protected int x = 123;
}
class B : A
{
static void Main()
{
var a = new A();
var b = new B();
La instrucción a.x = 10 genera un error porque se ha creado en el método estático Main y no en una instancia
de clase B.
Los miembros de estructura no se pueden proteger porque la estructura no puede heredarse.
Ejemplo 2
En este ejemplo, la clase DerivedPoint se deriva de Point . Por lo tanto, puede acceder a los miembros
protegidos de la clase base directamente desde la clase derivada.
class Point
{
protected int x;
protected int y;
}
Si cambia los niveles de acceso de x y y a private, el compilador genera los mensajes de error:
'Point.y' is inaccessible due to its protection level.
Vea también
Referencia de C#
Guía de programación de C#
Palabras clave de C#
Modificadores de acceso
Niveles de accesibilidad
Modificadores
public
private
internal
Security concerns for internal virtual keywords (Problemas de seguridad de palabras clave virtuales internas)
public (Referencia de C#)
16/09/2021 • 2 minutes to read
La palabra clave public es un modificador de acceso para tipos y miembros de tipo. El acceso público es el
nivel de acceso más permisivo. No hay ninguna restricción para el acceso a miembros públicos, como en este
ejemplo:
class SampleClass
{
public int x; // No access restrictions.
}
Ejemplo
En el ejemplo siguiente, se declaran dos clases, PointTest y Program . Se obtiene acceso a los miembros
públicos x e y de PointTest directamente desde Program .
class PointTest
{
public int x;
public int y;
}
class Program
{
static void Main()
{
var p = new PointTest();
// Direct access to public members.
p.x = 10;
p.y = 15;
Console.WriteLine($"x = {p.x}, y = {p.y}");
}
}
// Output: x = 10, y = 15
Si cambia el nivel de acceso public a private o protected, obtendrá el siguiente mensaje de error:
'PointTest.y' no es accesible debido a su nivel de protección.
Vea también
Referencia de C#
Guía de programación de C#
Modificadores de acceso
Palabras clave de C#
Modificadores de acceso
Niveles de accesibilidad
Modificadores
private
protected
internal
protected internal (Referencia de C#)
16/09/2021 • 2 minutes to read
Ejemplo
Se puede obtener acceso a un miembro protected internal de una clase base desde cualquier tipo de
ensamblado que lo contenga. También estará accesible en una clase derivada ubicada en otro ensamblado, pero
solo si el acceso se produce a través de una variable del tipo de clase derivada. Por ejemplo, vea el siguiente
segmento de código:
// Assembly1.cs
// Compile with: /target:library
public class BaseClass
{
protected internal int myValue = 0;
}
class TestAccess
{
void Access()
{
var baseObject = new BaseClass();
baseObject.myValue = 5;
}
}
// Assembly2.cs
// Compile with: /reference:Assembly1.dll
class DerivedClass : BaseClass
{
static void Main()
{
var baseObject = new BaseClass();
var derivedObject = new DerivedClass();
Este ejemplo contiene dos archivos, Assembly1.cs y Assembly2.cs . El primer archivo contiene una clase base
interna, BaseClass , y otra clase, TestAccess . BaseClass posee un miembro protected internal ( myValue ), al que
se obtiene acceso por medio del tipo TestAccess . En el segundo archivo, un intento de tener acceso a myValue
a través de una instancia de BaseClass generará un error, mientras que un acceso a este miembro a través de
una instancia de una clase derivada ( DerivedClass ) se realizará correctamente.
Los miembros de struct no pueden ser protected internal , porque los structs no se heredan.
Vea también
Referencia de C#
Guía de programación de C#
Palabras clave de C#
Modificadores de acceso
Niveles de accesibilidad
Modificadores
public
private
internal
Security concerns for internal virtual keywords (Problemas de seguridad de palabras clave virtuales internas)
private protected (Referencia de C#)
16/09/2021 • 2 minutes to read
La combinación de palabras claves private protected es un modificador de acceso de miembro. Los miembros
private protected están accesibles para los tipos que se deriven de la clase contenedora, pero solo desde dentro
del ensamblado correspondiente que lo contenga. Para obtener una comparación de private protected con los
demás modificadores de acceso, vea Niveles de accesibilidad.
NOTE
El modificador de acceso private protected es válido en C# versión 7.2 y versiones posteriores.
Ejemplo
Se puede tener acceso a un miembro private protected de una clase base desde tipos derivados en el
ensamblado que lo contiene solo si el tipo estático de la variable es el tipo de clase derivada. Por ejemplo, vea el
siguiente segmento de código:
// Assembly2.cs
// Compile with: /reference:Assembly1.dll
class DerivedClass2 : BaseClass
{
void Access()
{
// Error CS0122, because myValue can only be
// accessed by types in Assembly1
// myValue = 10;
}
}
Este ejemplo contiene dos archivos, Assembly1.cs y Assembly2.cs . El primer archivo contiene una clase base
pública, BaseClass , y un tipo derivado de ella, DerivedClass1 . BaseClass posee un miembro private protected,
myValue , al que DerivedClass1 intenta tener acceso de dos maneras. El primer intento de acceso a myValue a
través de una instancia de BaseClass generará un error. En cambio, el intento de usarlo como un miembro
heredado en DerivedClass1 se realizará correctamente.
En el segundo archivo, un intento de tener acceso a myValue como un miembro heredado de DerivedClass2
generará un error, ya que solo está accesible para los tipos derivados en Assembly1.
Si Assembly1.cs contiene un elemento InternalsVisibleToAttribute que asigna un nombre a Assembly2 , la clase
derivada DerivedClass2 tendrá acceso a los miembros private protected declarados en BaseClass .
InternalsVisibleTo hace que los miembros private protected sean visibles para las clases derivadas en otros
ensamblados.
Los miembros de struct no pueden ser private protected , porque los structs no se heredan.
Consulte también
Referencia de C#
Guía de programación de C#
Palabras clave de C#
Modificadores de acceso
Niveles de accesibilidad
Modificadores
public
private
internal
Security concerns for internal virtual keywords (Problemas de seguridad de palabras clave virtuales internas)
abstract (Referencia de C#)
16/09/2021 • 3 minutes to read
El modificador abstract indica que lo que se modifica carece de implementación o tiene una implementación
incompleta. El modificador abstract puede usarse con clases, métodos, propiedades, indexadores y eventos. Use
el modificador abstract en una declaración de clase para indicar que una clase está diseñada como clase base
de otras clases, no para crear instancias por sí misma. Los miembros marcados como abstractos deben
implementarse con clases no abstractas derivadas de la clase abstracta.
Ejemplo 1
En este ejemplo, la clase Square debe proporcionar una implementación de GetArea porque se deriva de
Shape :
Una clase no abstracta que derive de una clase abstracta debe incluir implementaciones reales de todos
los descriptores de acceso y métodos abstractos heredados.
Use el modificador abstract en una declaración de método o de propiedad para indicar que el método o la
propiedad no contienen implementación.
Los métodos abstractos tienen las siguientes características:
Un método abstracto es, implícitamente, un método virtual.
Solo se permiten declaraciones de métodos abstractos en clases abstractas.
Dado que una declaración de método abstracto no proporciona una implementación real, no hay ningún
cuerpo de método; la declaración de método finaliza simplemente con un punto y coma y no hay llaves ({
}) después de la firma. Por ejemplo:
interface I
{
void M();
}
abstract class C : I
{
public abstract void M();
}
Ejemplo 2
En este ejemplo, la clase DerivedClass se deriva de una clase abstracta BaseClass . La clase abstracta contiene
un método abstracto, AbstractMethod , y dos propiedades abstractas, X y Y .
abstract class BaseClass // Abstract class
{
protected int _x = 100;
protected int _y = 150;
public abstract void AbstractMethod(); // Abstract method
public abstract int X { get; }
public abstract int Y { get; }
}
En el ejemplo anterior, si intenta crear una instancia de la clase abstracta mediante una instrucción como esta:
Se mostrará un mensaje de error en el que se indica que el compilador no puede crear una instancia de la clase
abstracta "BaseClass".
Vea también
Referencia de C#
Guía de programación de C#
Modificadores
virtual
override
Palabras clave de C#
async (Referencia de C#)
16/09/2021 • 4 minutes to read
Use el modificador async para especificar que un método, una expresión lambda o un método anónimo es
asincrónico. Si usa este modificador en un método o una expresión, se hace referencia al mismo como un
método asincrónico. En el ejemplo siguiente se define un método asincrónico denominado ExampleMethodAsync :
Si no está familiarizado con la programación asincrónica o no entiende cómo un método asincrónico usa el
operador await para hacer el trabajo de larga duración sin bloquear el subproceso del autor de la llamada, lea
la introducción de Programación asincrónica con async y await. El siguiente código se encuentra dentro de un
método asincrónico y llama al método HttpClient.GetStringAsync:
Un método asincrónico se ejecuta sincrónicamente hasta alcanzar la primera expresión await , en la que se
suspende el método hasta que se complete la tarea en espera. Mientras tanto, el control vuelve al llamador del
método, como se muestra en el ejemplo de la sección siguiente.
Si el método que la palabra clave async modifica no contiene una expresión o instrucción await , el método se
ejecuta de forma sincrónica. Una advertencia del compilador alerta de cualquier método asincrónico que no
contenga instrucciones de await , porque esa situación podría indicar un error. Vea Advertencia del compilador
(nivel 1) CS4014.
La palabra clave async es contextual en el sentido de que es una palabra clave cuando modifica un método, una
expresión lambda o un método anónimo. En todos los demás contextos, se interpreta como identificador.
Ejemplo
En el ejemplo siguiente se muestra la estructura y el flujo de control entre un controlador de eventos
asincrónicos, StartButton_Click , y un método asincrónico, ExampleMethodAsync . El resultado del método
asincrónico es el número de caracteres de una página web. El código es adecuado para una aplicación Windows
Presentation Foundation (WPF) o de la Tienda Windows creada en Visual Studio; vea los comentarios del código
para configurar la aplicación.
Puede ejecutar este código en Visual Studio como una aplicación Windows Presentation Foundation (WPF) o
una aplicación de la Tienda Windows. Necesita un control de botón denominado StartButton y un control de
cuadro de texto denominado ResultsTextBox . Recuerde establecer los nombres y el controlador de manera que
tenga algo similar a esto:
try
{
int length = await ExampleMethodAsync();
// Note that you could put "await ExampleMethodAsync()" in the next line where
// "length" is, but due to when '+=' fetches the value of ResultsTextBox, you
// would not see the global side effect of ExampleMethodAsync setting the text.
ResultsTextBox.Text += String.Format("Length: {0:N0}\n", length);
}
catch (Exception)
{
// Process the exception if one occurs.
}
}
IMPORTANT
Para obtener más información sobre las tareas y el código que se ejecuta mientras se espera la finalización de una tarea,
vea Programación asincrónica con async y await. Para ver un ejemplo completo de la consola que usa elementos similares,
consulte el artículo Iniciar varias tareas asincrónicas y procesarlas a medida que se completan (C#).
El método asincrónico no puede declarar ningún parámetro in, ref o out, ni puede tener un valor devuelto de
referencia, pero puede llamar a los métodos que tienen estos parámetros.
Se puede especificar Task<TResult> como el tipo de valor devuelto de un método asincrónico si la instrucción
return del método especifica un operando de tipo TResult . Utilice Task si no se devuelve ningún valor
significativo al completarse el método. Es decir, una llamada al método devuelve Task , pero cuando se
completa Task , las expresiones await que esperan a que Task finalice se evalúan como void .
El tipo devuelto void se utiliza principalmente para definir controladores de eventos, que requieren ese tipo
devuelto. El llamador de un método asincrónico que devuelva void no puede esperar a que finalice y no puede
detectar las excepciones que el método inicia.
A partir de C# 7.0, devuelve otro tipo, normalmente un tipo de valor, que tiene un método GetAwaiter para
minimizar las asignaciones de memoria en secciones críticas de rendimiento del código.
Para más información y ejemplos, vea Tipos de valor devueltos asincrónicos.
Vea también
AsyncStateMachineAttribute
await
Programación asincrónica con async y await
Procesamiento de tareas asincrónicas a medida que se completan
const (Referencia de C#)
16/09/2021 • 2 minutes to read
La palabra clave const se usa para declarar un campo constante o una local constante. Los campos y locales
constantes no son variables y no se pueden modificar. Las constantes pueden ser números, valores booleanos,
cadenas o una referencia nula. No cree una constante para representar información que esperas que cambie en
algún momento. Por ejemplo, no use un campo constante para almacenar el precio de un servicio, un número
de versión de producto o el nombre comercial de una compañía. Estos valores pueden cambiar con el tiempo y,
como los compiladores propagan las constantes, otro código compilado con sus bibliotecas tendrán que volver
a compilarse para ver los cambios. Vea también la palabra clave readonly. Por ejemplo:
const int X = 0;
public const double GravitationalConstant = 6.673e-11;
private const string ProductName = "Visual C#";
A partir de C# 10, las cadenas interpoladas pueden ser constantes, si todas las expresiones utilizadas también
son cadenas constantes. Esta característica puede mejorar el código que compila cadenas constantes:
Observaciones
El tipo de una declaración constante especifica el tipo de los miembros que la declaración presenta. El
inicializador de una local constante o de un campo constante debe ser una expresión constante que se pueda
convertir implícitamente al tipo de destino.
Una expresión constante es una expresión que se puede evaluar por completo en tiempo de compilación. Por lo
tanto, los únicos valores posibles para las constantes de tipos de referencia son string y una referencia nula.
La declaración de constante puede declarar varias constantes, tales como:
Ejemplos
public class ConstTest
{
class SampleClass
{
public int x;
public int y;
public const int C1 = 5;
public const int C2 = C1 + 5;
Este ejemplo demuestra cómo usar las constantes como variables locales.
Vea también
Referencia de C#
Guía de programación de C#
Palabras clave de C#
Modificadores
readonly
event (Referencia de C#)
16/09/2021 • 2 minutes to read
La palabra clave event se usa para declarar un evento en una clase de publicador.
Ejemplo
En el ejemplo siguiente se muestra cómo declarar y generar un evento que usa EventHandler como el tipo de
delegado subyacente. Para obtener el código de ejemplo completo que también muestra cómo usar el tipo
delegado EventHandler<TEventArgs> genérico y cómo suscribirse a un evento y crear un método de
controlador de evento, vea Procedimiento para publicar eventos que cumplan las directrices de .NET.
Los eventos son un tipo especial de delegado de multidifusión que solo se pueden invocar desde la clase o el
struct en la que se declaran (la clase de publicador). Si otras clases o structs se suscriben al evento, se llamará a
sus métodos de controlador de eventos cuando la clase de publicador genera el evento. Para más información y
ejemplos de código, vea Eventos y Delegados.
Las constantes pueden marcarse como public, private, protected, internal, protected internal o private protected.
Estos modificadores de acceso definen cómo los usuarios de la clase pueden obtener acceso al evento. Para
obtener más información, consulte Modificadores de acceso.
static Hace que el evento esté disponible Clases estáticas y sus miembros
para los llamadores en cualquier
momento, aunque no exista ninguna
instancia de la clase.
PA L A B RA C L AVE DESC RIP C IÓ N PA RA O BT EN ER M Á S IN F O RM A C IÓ N
Un evento puede declararse como evento estático mediante la palabra clave static. Esto hace que el evento esté
disponible para los llamadores en cualquier momento, aunque no exista ninguna instancia de la clase. Para más
información, vea Clases estáticas y sus miembros.
Un evento puede marcarse como virtual mediante la palabra clave virtual. Esto permite que las clases derivadas
invaliden el comportamiento de eventos mediante la palabra clave override. Para obtener más información, vea
Herencia. Un evento que reemplaza un evento virtual también puede ser sealed, que especifica que ya no es
virtual para las clases derivadas. Por último, se puede declarar un evento como abstract, lo que significa que el
compilador no generará los bloques de descriptor de acceso de eventos add y remove . Por tanto, las clases
derivadas deben proporcionar una implementación propia.
Consulte también
Referencia de C#
Guía de programación de C#
Palabras clave de C#
add
remove
Modificadores
Procedimiento para combinar delegados (delegados de multidifusión)
extern (Referencia de C#)
16/09/2021 • 2 minutes to read
El modificador extern se usa para declarar un método que se implementa externamente. Un uso común del
modificador extern es con el atributo DllImport al usar servicios de interoperabilidad para llamar a código no
administrado. En este caso, el método se debe declarar también como static , como se muestra en el ejemplo
siguiente:
[DllImport("avifil32.dll")]
private static extern void AVIFileInit();
La palabra clave extern también puede definir un alias del ensamblado externo, lo que permite hacer
referencia a diferentes versiones del mismo componente desde un único ensamblado. Para obtener más
información, vea alias externo.
Es un error usar los modificadores abstract y extern juntos para modificar el mismo miembro. El uso del
modificador extern significa que el método se implementa fuera del código de C#, mientras que el uso del
modificador abstract significa que la implementación del método no se proporciona en la clase.
La palabra clave extern tiene usos más limitados en C# que en C++. Para comparar la palabra clave de C# con la
de C++, consulte el tema sobre el uso de extern para especificar vinculación en la referencia del lenguaje C++.
Ejemplo 1
En este ejemplo, el programa recibe una cadena del usuario y la muestra en un cuadro de mensaje. El programa
usa el método MessageBox importado de la biblioteca User32.dll.
//using System.Runtime.InteropServices;
class ExternTest
{
[DllImport("User32.dll", CharSet=CharSet.Unicode)]
public static extern int MessageBox(IntPtr h, string m, string c, int type);
Ejemplo 2
En este ejemplo se muestra un programa de C# que llama a una biblioteca de C (una DLL nativa).
1. Cree el archivo de C siguiente y denomínelo cmdll.c :
// cmdll.c
// Compile with: -LD
int __declspec(dllexport) SampleMethod(int i)
{
return i*10;
}
2. Abra una ventana del símbolo del sistema de las herramientas nativas de Visual Studio x64 (o x32) desde
el directorio de instalación de Visual Studio y compile el archivo cmdll.c escribiendo cl -LD cmdll.c en
el símbolo del sistema.
3. En el mismo directorio, cree el siguiente archivo de C# y denomínelo cm.cs :
// cm.cs
using System;
using System.Runtime.InteropServices;
public class MainClass
{
[DllImport("Cmdll.dll")]
public static extern int SampleMethod(int x);
4. Abra una ventana del símbolo del sistema de las herramientas nativas de Visual Studio x64 (o x32) del
directorio de instalación de Visual Studio y compile el archivo cm.cs escribiendo:
csc cm.cs (para el símbolo del sistema x64), o bien csc -platform:x86 cm.cs (para el símbolo del
sistema x32)
Vea también
System.Runtime.InteropServices.DllImportAttribute
Referencia de C#
Guía de programación de C#
Palabras clave de C#
Modificadores
in (Modificador genérico) (Referencia de C#)
16/09/2021 • 2 minutes to read
Para los parámetros de tipo genérico, la palabra clave in especifica que el parámetro de tipo es contravariante.
Puede usar la palabra clave in en las interfaces y delegados genéricos.
La contravarianza permite usar un tipo menos derivado que el que se especifica en el parámetro genérico. Esto
permite la conversión implícita de las clases que implementan interfaces contravariantes y la conversión
implícita de los tipos de delegado. La covarianza y la contravarianza de los parámetros de tipo genérico son
compatibles con los tipos de referencia, pero no lo son con los tipos de valor.
Un tipo se puede declarar contravariante en una interfaz o un delgado genéricos solo si define el tipo de
parámetros de un método y no el tipo de valor devuelto de un método. Los parámetros In , ref y out deben
ser invariables, es decir, ni covariantes ni contravariantes.
Una interfaz que tiene un parámetro de tipo contravariante permite que sus métodos acepten argumentos de
tipos menos derivados que los que se especifican en el parámetro de tipo de interfaz. Por ejemplo, en la interfaz
IComparer<T>, el tipo T es contravariante, puede asignar un objeto de tipo IComparer<Person> a un objeto de
tipo IComparer<Employee> sin tener que usar ningún método de conversión especial si Employee hereda Person .
A un delegado contravariante se le puede asignar otro delegado del mismo tipo, pero con un parámetro de tipo
genérico menos derivado.
Para obtener más información, vea Covarianza y contravarianza.
// Contravariant interface.
interface IContravariant<in A> { }
class Program
{
static void Test()
{
IContravariant<Object> iobj = new Sample<Object>();
IContravariant<String> istr = new Sample<String>();
// Contravariant delegate.
public delegate void DContravariant<in A>(A argument);
Vea también
out
Covarianza y contravarianza
Modificadores
new (Modificador, Referencia de C#)
16/09/2021 • 3 minutes to read
Cuando se utiliza como modificador de una declaración, la palabra clave new oculta explícitamente un miembro
heredado de una clase base. Cuando se oculta un miembro heredado, la versión derivada del miembro
reemplaza a la versión de la clase base. Esto supone que la versión de clase base del miembro es visible, ya que
ya estaría oculta si se hubiera marcado como private o, en algunos casos, como internal . Aunque los
miembros public o protected se pueden ocultar sin utilizar el modificador new , se generará una advertencia
del compilador. Si utiliza new explícitamente para ocultar un miembro, se suprime esta advertencia.
También puede usar la palabra clave new para crear una instancia de un tipo o como una restricción de tipo
genérico.
Para ocultar un miembro heredado, declárelo en la clase derivada con el mismo nombre de miembro y
modifíquelo con la palabra clave new . Por ejemplo:
En este ejemplo, una clase anidada oculta una clase que tiene el mismo nombre en la clase base. El ejemplo
muestra cómo utilizar el modificador new para eliminar el mensaje de advertencia y cómo obtener acceso a los
miembros de la clase oculta mediante sus nombres completos.
public class BaseC
{
public class NestedC
{
public int x = 200;
public int y;
}
}
Console.WriteLine(c1.x);
Console.WriteLine(c2.x);
}
}
/*
Output:
100
200
*/
Si quita el modificador new , el programa seguirá compilándose y ejecutándose, pero aparecerá la siguiente
advertencia:
The keyword new is required on 'MyDerivedC.x' because it hides inherited member 'MyBaseC.x'.
Vea también
Referencia de C#
Guía de programación de C#
Palabras clave de C#
Modificadores
Control de versiones con las palabras clave Override y New
Saber cuándo utilizar las palabras clave Override y New
out (Modificador genérico) (Referencia de C#)
16/09/2021 • 2 minutes to read
Para los parámetros de tipo genérico, la palabra clave out especifica que el parámetro de tipo es covariante.
Puede usar la palabra clave out en las interfaces y delegados genéricos.
La covarianza le permite usar un tipo más derivado que el que se especifica en el parámetro genérico. Esto
permite la conversión implícita de las clases que implementan interfaces covariantes y la conversión implícita de
los tipos de delegado. La covarianza y la contravarianza son compatibles con los tipos de referencia, pero no lo
son con los tipos de valor.
Una interfaz con un parámetro de tipo covariante permite que sus métodos devuelvan tipos más derivados que
los especificados por el parámetro de tipo. Por ejemplo, dado que en .NET Framework 4, en IEnumerable<T>, el
tipo T es covariante, puede asignar un objeto del tipo IEnumerable(Of String) a otro objeto del tipo
IEnumerable(Of Object) sin usar ningún método de conversión especial.
A un delegado covariante se le puede asignar otro delegado del mismo tipo, pero con un parámetro de tipo
genérico más derivado.
Para obtener más información, vea Covarianza y contravarianza.
// Covariant interface.
interface ICovariant<out R> { }
class Program
{
static void Test()
{
ICovariant<Object> iobj = new Sample<Object>();
ICovariant<String> istr = new Sample<String>();
En una interfaz genérica, un parámetro de tipo se puede declarar como covariante si cumple las siguientes
condiciones:
El parámetro de tipo se usa solamente como tipo de valor devuelto de los métodos de interfaz y no como
tipo de los argumentos de método.
NOTE
Hay una excepción para esta regla. Si en una interfaz covariante tiene un delegado genérico contravariante como
parámetro de método, puede usar el tipo covariante como parámetro de tipo genérico para este delegado. Para
obtener más información sobre los delegados genéricos covariantes y contravariantes, vea Varianza en delegados
y Usar la varianza para los delegados genéricos Func y Action.
El parámetro de tipo no se usa como restricción genérica para los métodos de interfaz.
// Covariant delegate.
public delegate R DCovariant<out R>();
En un delegado genérico, un tipo se puede declarar como covariante si se usa solamente como tipo de valor
devuelto por un método y no se usa para los argumentos de método.
Vea también
Varianza en interfaces genéricas
in
Modificadores
override (Referencia de C#)
16/09/2021 • 3 minutes to read
Un método override proporciona una nueva implementación de un método heredado de una clase base. El
método invalidado por una declaración override se conoce como método base invalidado. Un método
override debe tener la misma signatura que el método base invalidado. A partir de C# 9.0, los métodos
override admiten tipos de valor devuelto covariantes. En concreto, el tipo de valor devuelto de un método
override puede derivar del tipo de valor devuelto del método base correspondiente. En C# 8.0 y versiones
anteriores, los tipos de valor devuelto de un método override y el método base invalidado deben ser iguales.
No se puede invalidar un método estático o no virtual. El método base invalidado debe ser virtual , abstract
o override .
Una declaración override no puede cambiar la accesibilidad del método virtual . El método override y el
método virtual deben tener el mismo modificador de nivel de acceso.
No se pueden usar los modificadores new , static o virtual para modificar un método override .
Una declaración de propiedad de invalidación debe especificar exactamente el mismo modificador de acceso,
tipo y nombre que la propiedad heredada,. A partir de C# 9.0, las propiedades de invalidación de solo lectura
admiten tipos de valor devuelto covariantes. La propiedad invalidada debe ser virtual , abstract u override .
Para obtener más información sobre cómo usar la palabra clave override , vea Control de versiones con las
palabras clave Override y New y Saber cuándo usar las palabras clave Override y New (Guía de programación
de C#). Para obtener información sobre la herencia, vea Herencia.
Ejemplo
En este ejemplo se define una clase base denominada Employee y una clase derivada denominada
SalesEmployee . La clase SalesEmployee incluye un campo adicional, salesbonus , e invalida el método
CalculatePay para tenerlo en cuenta.
class TestOverride
{
public class Employee
{
public string name;
Vea también
Referencia de C#
Herencia
Palabras clave de C#
Modificadores
abstract
virtual
new (modificador)
Polimorfismo
readonly (Referencia de C#)
16/09/2021 • 4 minutes to read
WARNING
Un tipo visible externamente que contenga un campo de solo lectura visible externamente que sea un tipo de
referencia mutable puede ser una vulnerabilidad de seguridad y puede desencadenar la advertencia CA2104: "No
declarar tipos de referencias mutables de solo lectura".
En una definición de tipo de readonly struct , readonly indica que el tipo de estructura es inmutable.
Para obtener más información, vea la sección struct readonly del artículo tipos de estructura.
En una declaración de miembro de instancia dentro de un tipo de estructura, readonly indica que un
miembro de instancia no modifica el estado de la estructura. Para obtener más información, vea la
sección Miembros de instancia readonly del artículo Tipos de estructura.
En una devolución del método ref readonly , el modificador readonly indica que el método devuelve
una referencia y las operaciones de escritura no se permiten en esa referencia.
Los contextos readonly struct y ref readonly se han agregado en C# 7.2. Los miembros de estructura
readonly se han agregado en C# 8.0.
NOTE
La palabra clave readonly es diferente de la palabra clave const. Un campo const solo se puede inicializar en la
declaración del campo. Un campo readonly se puede asignar varias veces en la declaración de campo y en cualquier
constructor. Por lo tanto, los campos readonly pueden tener diferentes valores en función del constructor que se use.
Además, mientras que un campo const es una constante en tiempo de compilación, el campo readonly puede usarse
para constantes en tiempo de ejecución, como muestra el siguiente ejemplo:
public SamplePoint()
{
// Initialize a readonly instance field
z = 24;
}
No es necesario que el tipo devuelto sea una readonly struct . Cualquier tipo que pueda devolver ref también
puede devolver ref readonly .
Vea también
Referencia de C#
Guía de programación de C#
Palabras clave de C#
Modificadores
const
Campos
sealed (Referencia de C#)
16/09/2021 • 2 minutes to read
Cuando se aplica a una clase, el modificador sealed impide que otras clases hereden de ella. En el ejemplo
siguiente, la clase B hereda de la clase A , pero ninguna clase puede heredar de la clase B .
class A {}
sealed class B : A {}
También puede usar el modificador sealed en un método o propiedad que invalida un método o propiedad
virtual en una clase base. De este modo, puede permitir que las clases deriven de su clase e impedir que
invaliden determinados métodos o propiedades virtuales.
Ejemplo
En el ejemplo siguiente, Z hereda de Y pero Z no puede invalidar la función virtual F que se declara en X
y se sella en Y .
class X
{
protected virtual void F() { Console.WriteLine("X.F"); }
protected virtual void F2() { Console.WriteLine("X.F2"); }
}
class Y : X
{
sealed protected override void F() { Console.WriteLine("Y.F"); }
protected override void F2() { Console.WriteLine("Y.F2"); }
}
class Z : Y
{
// Attempting to override F causes compiler error CS0239.
// protected override void F() { Console.WriteLine("Z.F"); }
// Overriding F2 is allowed.
protected override void F2() { Console.WriteLine("Z.F2"); }
}
Al definir nuevos métodos o propiedades en una clase, puede impedir que las clases derivadas los invaliden.
Para ello, no los declare como virtuales.
Es un error usar el modificador abstract con una clase sellada, porque una clase abstracta debe heredarla una
clase que proporcione una implementación de los métodos o propiedades abstractos.
Cuando se aplica a un método o propiedad, el modificador sealed siempre se debe usar con override.
Dado que los structs están sellados implícitamente, no puede heredarse.
Para obtener más información, vea Herencia.
Para obtener más ejemplos, vea Clases y miembros de clase abstractos y sellados.
sealed class SealedClass
{
public int x;
public int y;
}
class SealedTest2
{
static void Main()
{
var sc = new SealedClass();
sc.x = 110;
sc.y = 150;
Console.WriteLine($"x = {sc.x}, y = {sc.y}");
}
}
// Output: x = 110, y = 150
En el ejemplo anterior, podría intentar heredar de la clase sellada mediante la instrucción siguiente:
class MyDerivedC: SealedClass {} // Error
Comentarios
Para determinar si se debe sellar una clase, un método o una propiedad, por lo general debe tener en cuenta los
dos puntos siguientes:
Las posibles ventajas que podrían obtener las clases derivadas con la capacidad de personalizar la clase.
La posibilidad de que las clases derivadas modifiquen las clases de tal manera que no funcionen
correctamente o del modo esperado.
Consulte también
Referencia de C#
Guía de programación de C#
Palabras clave de C#
Clases estáticas y sus miembros
Clases y miembros de clase abstractos y sellados
Modificadores de acceso
Modificadores
override
virtual
static (Referencia de C#)
16/09/2021 • 3 minutes to read
En esta página se trata la palabra clave del modificador static . La palabra clave static también forma parte
de la directiva using static .
Use el modificador static para declarar un miembro estático, que pertenece al propio tipo en lugar de a un
objeto específico. El modificador static se puede usar para declarar clases static . En las clases, las interfaces
y las estructuras, puede agregar el modificador static a los campos, los métodos, las propiedades, los
operadores, los eventos y los constructores. El modificador static no se puede usar con indizadores ni
finalizadores. Para más información, vea Clases estáticas y sus miembros.
A partir de C# 8,0, puede agregar el modificador static a una función local. Una función local estática no
puede capturar variables locales o el estado de la instancia.
A partir de C# 9.0, puede agregar el modificador static a una expresión lambda o un método anónimo. Una
expresión lambda o un método anónimo estático no pueden capturar variables locales o el estado de la
instancia.
Una declaración de constantes o tipos es implícitamente un miembro static . No se puede hacer referencia a
un miembro static mediante una instancia, sino que se hace a través del nombre de tipo. Por ejemplo,
considere la siguiente clase:
Para hacer referencia al miembro static x , use el nombre completo, MyBaseC.MyStruct.x , a menos que el
miembro sea accesible desde el mismo ámbito:
Console.WriteLine(MyBaseC.MyStruct.x);
Mientras que una instancia de una clase contiene una copia independiente de todos los campos de instancia de
la clase, solo hay una copia de cada campo static .
No se puede usar this para hacer referencia a métodos static o descriptores de acceso de propiedades.
Si la palabra clave static se aplica a una clase, todos los miembros de esta deben ser static .
Las clases, las interfaces y las clases static pueden tener constructores static . Se llama a un constructor
static en algún momento entre el inicio del programa y la creación de una instancia de la clase.
NOTE
La palabra clave static tiene usos más limitados que en C++. Para ver una comparación con la palabra clave de C++,
vea Clases de almacenamiento (C++).
Para mostrar miembros static , es recomendable una clase que represente al empleado de una empresa.
Supongamos que la clase contiene un método de recuento de empleados y un campo para almacenar el
número de empleados. El método y el campo no pertenecen a ninguna instancia de ningún empleado, sino que
pertenecen a la clase de empleados en su conjunto. Se deben declarar como miembros static de la clase.
public Employee4()
{
}
Test.x = 99;
Console.WriteLine(Test.x);
}
}
/*
Output:
0
5
99
*/
Vea también
Referencia de C#
Guía de programación de C#
Palabras clave de C#
Modificadores
using static (directiva)
Clases estáticas y sus miembros
unsafe (Referencia de C#)
16/09/2021 • 2 minutes to read
La palabra clave unsafe denota un contexto no seguro, que es necesario para realizar cualquier operación que
implique punteros. Para obtener más información, vea Código no seguro y punteros (Guía de programación de
C#).
Puede usar el codificador unsafe en la declaración de un tipo o miembro. Por consiguiente, toda la extensión
textual del tipo o miembro se considera un contexto no seguro. Por ejemplo, el siguiente método se declara con
el modificador unsafe :
El ámbito del contexto no seguro se extiende desde la lista de parámetros hasta el final del método, por lo que
también pueden usarse punteros en la lista de parámetros:
unsafe static void FastCopy ( byte* ps, byte* pd, int count ) {...}
También puede usarse un bloque no seguro para habilitar el uso de código no seguro en el bloque. Por ejemplo:
unsafe
{
// Unsafe context: can use pointers here.
}
Para compilar código no seguro, debe especificar la opción del compilador AllowUnsafeBlocks . Common
Language Runtime no puede comprobar el código no seguro.
Ejemplo
// compile with: -unsafe
class UnsafeTest
{
// Unsafe method: takes pointer to int.
unsafe static void SquarePtrParam(int* p)
{
*p *= *p;
}
Vea también
Referencia de C#
Guía de programación de C#
Palabras clave de C#
fixed (instrucción)
Código no seguro y punteros
Búferes de tamaño fijo
virtual (Referencia de C#)
16/09/2021 • 3 minutes to read
La palabra clave virtual se usa para modificar una declaración de método, propiedad, indizador o evento y
permitir que se invalide en una clase derivada. Por ejemplo, cualquier clase que herede este método puede
reemplazarlo:
Un miembro de reemplazo de una clase derivada puede modificar la implementación de un miembro virtual.
Para obtener más información sobre cómo usar la palabra clave virtual , vea Control de versiones con las
palabras clave Override y New y Saber cuándo usar las palabras clave Override y New (Guía de programación
de C#).
Observaciones
Cuando se invoca a un método virtual, se busca un miembro de reemplazo en el tipo en tiempo de ejecución del
objeto. Se llama al miembro de reemplazo en la clase más derivada, que podría ser el miembro original si
ninguna clase derivada ha invalidado al miembro.
De forma predeterminada, los métodos son no virtuales. No se puede invalidar un método no virtual.
El modificador virtual no se puede usar con los modificadores static , abstract , private o override . En el
siguiente ejemplo se muestra una propiedad virtual:
class MyBaseClass
{
// virtual auto-implemented property. Overrides can only
// provide specialized behavior if they implement get and set accessors.
public virtual string Name { get; set; }
Las propiedades virtuales se comportan como métodos virtuales, salvo por las diferencias en la sintaxis de
declaración e invocación.
Es un error usar el modificador virtual en una propiedad estática.
Una propiedad virtual heredada se puede invalidar en una clase derivada al incluir una declaración de
propiedad que use el modificador override .
Ejemplo
En este ejemplo, la clase Shape contiene las dos coordenadas x , y y el método virtual Area() . Las distintas
clases de formas como Circle , Cylinder y Sphere heredan la clase Shape y se calcula el área de cada figura.
Cada clase derivada tiene su propia implementación de invalidación de Area() .
Observe que las clases heredadas Circle , Sphere y Cylinder usan constructores que inicializan la clase base,
como se muestra en la siguiente declaración.
El programa siguiente calcula y muestra el área apropiada de cada figura al invocar a la implementación
adecuada del método Area() , según el objeto asociado al método.
class TestClass
{
public class Shape
{
public const double PI = Math.PI;
protected double x, y;
public Shape()
{
}
Vea también
Polimorfismo
abstract
override
new (modificador)
volatile (Referencia de C#)
16/09/2021 • 3 minutes to read
La palabra clave volatile indica que un campo puede ser modificado por varios subprocesos que se ejecutan
al mismo tiempo. El compilador, el sistema de runtime e incluso el hardware pueden reorganizar las lecturas y
escrituras en las ubicaciones de memoria por motivos de rendimiento. Los campos que se declaran volatile
no están sujetos a estas optimizaciones. La incorporación del modificador volatile asegura que todos los
subprocesos observarán las operaciones de escritura volátiles realizadas por cualquier otro subproceso en el
orden en que se realizaron. No hay ninguna garantía de una única ordenación total de las operaciones de
escritura volátiles como se muestra en todos los subprocesos de ejecución.
NOTE
Al escribir en un campo marcado como volatile , la palabra clave volatile controla el orden en el que se realizan las
escrituras. No garantiza que estas escrituras sean visibles de inmediato para otros subprocesos.
Ejemplo
En el ejemplo siguiente se muestra cómo declarar una variable de campo pública como volatile .
class VolatileTest
{
public volatile int sharedStorage;
En el ejemplo siguiente se muestra cómo crear un subproceso auxiliar o de trabajo y usarlo para realizar el
procesamiento en paralelo con el del subproceso principal. Para más información sobre el multithreading, vea
Subprocesamiento administrado.
public class Worker
{
// This method is called when the thread is started.
public void DoWork()
{
bool work = false;
while (!_shouldStop)
{
work = !work; // simulate some work
}
Console.WriteLine("Worker thread: terminating gracefully.");
}
public void RequestStop()
{
_shouldStop = true;
}
// Keyword volatile is used as a hint to the compiler that this data
// member is accessed by multiple threads.
private volatile bool _shouldStop;
}
Con el modificador volatile que se agrega a la declaración de _shouldStop en su lugar, siempre obtendrá los
mismos resultados (similar al fragmento que se muestra en el código anterior). Sin embargo, sin ese
modificador en el miembro _shouldStop , el comportamiento es imprevisible. El método DoWork puede
optimizar el acceso a los miembros, lo que provoca la lectura de datos obsoletos. Dada la naturaleza de la
programación multiproceso, el número de lecturas obsoletas es imprevisible. Distintas ejecuciones del
programa generarán resultados ligeramente diferentes.
Vea también
Especificación del lenguaje C#: palabra clave volatile
Referencia de C#
Guía de programación de C#
Palabras clave de C#
Modificadores
lock (Instrucción)
Interlocked
Palabras clave de instrucciones (Referencia de C#)
16/09/2021 • 2 minutes to read
Las instrucciones son instrucciones de programa. A excepción de como se describe en los temas a los que se
hace referencia en la tabla siguiente, las instrucciones se ejecutan en secuencia. En la tabla siguiente, se
enumeran las palabras claves de instrucción de C#. Para obtener más información sobre las instrucciones que
no se expresan con ninguna palabra clave, consulte Instrucciones.
Vea también
Referencia de C#
Instrucciones
Palabras clave de C#
break (Referencia de C#)
16/09/2021 • 3 minutes to read
La instrucción break finaliza la ejecución del bucle contenedor más próximo o de la instrucción switch en la
que aparezca. El control se pasa a la instrucción que hay a continuación de la instrucción finalizada, si existe.
Ejemplo 1
En este ejemplo, la instrucción condicional contiene un contador que se supone que cuenta de 1 a 100. Pero la
instrucción break finaliza el bucle después de cuatro recuentos.
class BreakTest
{
static void Main()
{
for (int i = 1; i <= 100; i++)
{
if (i == 5)
{
break;
}
Console.WriteLine(i);
}
Ejemplo 2
En este ejemplo se muestra el uso de break en una instrucción switch .
class Switch
{
static void Main()
{
Console.Write("Enter your selection (1, 2, or 3): ");
string s = Console.ReadLine();
int n = Int32.Parse(s);
switch (n)
{
case 1:
Console.WriteLine("Current value is 1");
break;
case 2:
Console.WriteLine("Current value is 2");
break;
case 3:
Console.WriteLine("Current value is 3");
break;
default:
Console.WriteLine("Sorry, invalid selection.");
break;
}
Sample Output:
Enter your selection (1, 2, or 3): 1
Current value is 1
*/
Ejemplo 3
En este ejemplo, se usa la instrucción break para salir de un bucle anidado interno y devolver el control al bucle
externo. El control solo se devuelve un nivel hacia arriba en los bucles anidados.
class BreakInNestedLoops
{
static void Main(string[] args)
{
int[] numbers = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
char[] letters = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j' };
// Outer loop.
for (int i = 0; i < numbers.Length; i++)
{
Console.WriteLine($"num = {numbers[i]}");
// Inner loop.
for (int j = 0; j < letters.Length; j++)
{
if (j == i)
{
// Return control to outer loop.
break;
}
Console.Write($" {letters[j]} ");
}
Console.WriteLine();
}
/*
* Output:
num = 0
num = 1
a
num = 2
a b
num = 3
a b c
num = 4
a b c d
num = 5
a b c d e
num = 6
a b c d e f
num = 7
a b c d e f g
num = 8
a b c d e f g h
num = 9
a b c d e f g h i
*/
Ejemplo 4
En este ejemplo, la instrucción break solo se usa para salir de la rama actual durante cada iteración del bucle. El
propio bucle no se ve afectado por las instancias de break que pertenecen a la instrucción anidada switch .
class BreakFromSwitchInsideLoop
{
static void Main(string[] args)
{
// loop 1 to 3
for (int i = 1; i <= 3; i++)
{
switch(i)
{
case 1:
Console.WriteLine("Current value is 1");
break;
case 2:
Console.WriteLine("Current value is 2");
break;
case 3:
Console.WriteLine("Current value is 3");
break;
default:
Console.WriteLine("This shouldn't happen.");
break;
}
}
/*
* Output:
Current value is 1
Current value is 2
Current value is 3
*/
Vea también
Referencia de C#
Guía de programación de C#
Palabras clave de C#
Instrucción switch
continue (Referencia de C#)
16/09/2021 • 2 minutes to read
Ejemplo
En este ejemplo se inicializa un contador para contar del 1 al 10. Si se usa la instrucción continue junto con la
expresión (i < 9) , se omiten las instrucciones comprendidas entre continue y el final del cuerpo de for en
las iteraciones donde i es menor que 9. En las dos últimas iteraciones del bucle for (donde i == 9 e i == 10),
la instrucción continue no se ejecuta y el valor de i se imprime en la consola.
class ContinueTest
{
static void Main()
{
for (int i = 1; i <= 10; i++)
{
if (i < 9)
{
continue;
}
Console.WriteLine(i);
}
Vea también
Referencia de C#
Guía de programación de C#
Palabras clave de C#
break (Instrucción)
goto (Referencia de C#)
16/09/2021 • 2 minutes to read
La instrucción goto transfiere el control del programa directamente a una instrucción con etiqueta.
Un uso común de goto consiste en transferir el control a una etiqueta de switch case específica o a la etiqueta
predeterminada en una instrucción switch .
La instrucción goto también es útil para salir de bucles demasiado anidados.
Ejemplo 1
En el ejemplo siguiente se muestra cómo usar goto en una instrucción switch .
class SwitchTest
{
static void Main()
{
Console.WriteLine("Coffee sizes: 1=Small 2=Medium 3=Large");
Console.Write("Please enter your selection: ");
string s = Console.ReadLine();
int n = int.Parse(s);
int cost = 0;
switch (n)
{
case 1:
cost += 25;
break;
case 2:
cost += 25;
goto case 1;
case 3:
cost += 50;
goto case 1;
default:
Console.WriteLine("Invalid selection.");
break;
}
if (cost != 0)
{
Console.WriteLine($"Please insert {cost} cents.");
}
Console.WriteLine("Thank you for your business.");
Sample Output:
Coffee sizes: 1=Small 2=Medium 3=Large
Please enter your selection: 2
Please insert 50 cents.
Thank you for your business.
*/
Ejemplo 2
En el ejemplo siguiente se muestra cómo usar goto para salir de bucles anidados.
// Read input.
Console.Write("Enter the number to search for: ");
// Input a string.
string myNumber = Console.ReadLine();
// Search.
for (int i = 0; i < x; i++)
{
for (int j = 0; j < y; j++)
{
if (array[i, j].Equals(myNumber))
{
goto Found;
}
}
}
Found:
Console.WriteLine($"The number {myNumber} is found.");
Finish:
Console.WriteLine("End of search.");
Sample Output
Enter the number to search for: 44
The number 44 is found.
End of search.
*/
La instrucción return termina la ejecución del método en el que aparece y devuelve el control al método de
llamada. También puede devolver un valor opcional. Si el método es del tipo void , la instrucción return se
puede omitir.
Si la instrucción return está incluida en un bloque try , el bloque finally , si existe, se ejecutará antes de que el
control se devuelva al método de llamada.
Ejemplo
En el siguiente ejemplo, el método CalculateArea() devuelve la variable local area como un valor de tipo
double .
class ReturnTest
{
static double CalculateArea(int r)
{
double area = r * r * Math.PI;
return area;
}
Vea también
Referencia de C#
Guía de programación de C#
Palabras clave de C#
return (instrucción)
throw (Referencia de C#)
16/09/2021 • 3 minutes to read
Observaciones
La sintaxis de throw es la siguiente:
throw [e];
donde e es una instancia de una clase derivada de System.Exception. En el ejemplo siguiente se usa la
instrucción throw para producir una excepción IndexOutOfRangeException si el argumento pasado a un
método denominado GetNumber no se corresponde con un índice válido de una matriz interna.
using System;
namespace Throw2
{
public class NumberGenerator
{
int[] numbers = { 2, 4, 6, 8, 10, 12, 14, 16, 18, 20 };
Después, los autores de llamadas a método usan un bloque try-catch o try-catch-finally para controlar la
excepción generada. En el ejemplo siguiente se controla la excepción producida por el método GetNumber .
using System;
namespace Throw
{
public class Sentence
{
public Sentence(string s)
{
Value = s;
}
IMPORTANT
También puede usar la sintaxis throw e en un bloque catch para crear instancias de una nueva excepción que se pase
al autor de llamada. En este caso, no se conserva el seguimiento de la pila de la excepción original, que está disponible en
la propiedad StackTrace.
La expresión throw
A partir de C# 7.0, se puede usar throw como una expresión y como una instrucción. Esto permite iniciar una
excepción en contextos que antes no se admitían. Entre ellas se incluyen las siguientes:
El operador condicional. En el ejemplo siguiente se usa una expresión throw para iniciar una excepción
ArgumentException si se pasa a un método una matriz de cadena vacía. Antes de C# 7.0, esta lógica tenía
que aparecer en una instrucción if / else .
private static void DisplayFirstNumber(string[] args)
{
string arg = args.Length >= 1 ? args[0] :
throw new ArgumentException("You must supply an argument");
if (Int64.TryParse(arg, out var number))
Console.WriteLine($"You entered {number:F0}");
else
Console.WriteLine($"{arg} is not a number.");
}
El operador de uso combinado de NULL. En el ejemplo siguiente, se usa una expresión throw con un
operador de uso combinado de NULL para producir una excepción si la cadena asignada a una propiedad
Name es null .
Un método o lambda con forma de expresión. En el ejemplo siguiente se muestra un método con forma
de expresión que produce una excepción InvalidCastException porque no se admite una conversión a un
valor DateTime.
Vea también
Referencia de C#
Guía de programación de C#
try-catch
Palabras clave de C#
Cómo: Iniciar excepciones explícitamente
try-catch (Referencia de C#)
16/09/2021 • 9 minutes to read
La instrucción try-catch consta de un bloque try seguido de una o más cláusulas catch que especifican
controladores para diferentes excepciones.
Cuando se produce una excepción, Common Language Runtime (CLR) busca la instrucción catch que controla
esta excepción. Si el método que se ejecuta actualmente no contiene un bloque catch , CLR busca el método
que llamó el método actual, y así sucesivamente hasta la pila de llamadas. Si no existe ningún bloque catch ,
CLR muestra al usuario un mensaje de excepción no controlada y detiene la ejecución del programa.
El bloque try contiene el código protegido que puede producir la excepción. El bloque se ejecuta hasta que se
produce una excepción o hasta que se completa correctamente. Por ejemplo, el intento siguiente de convertir un
objeto null produce la excepción NullReferenceException:
object o2 = null;
try
{
int i2 = (int)o2; // Error
}
Aunque la cláusula catch puede utilizarse sin argumentos para detectar cualquier tipo de excepción, no se
recomienda este uso. En general, solo debe convertir las excepciones que sabe cómo recuperar. Por lo tanto,
debe especificar siempre un argumento de objeto derivado de System.Exception Por ejemplo:
catch (InvalidCastException e)
{
}
Es posible utilizar más de una cláusula catch específica en la misma instrucción try-catch. En este caso, el orden
de las cláusulas catch es importante, puesto que las cláusulas catch se examinan por orden. Detectar las
excepciones más específicas antes que las menos específicas. El compilador genera un error si ordena los
bloques de detección para que un bloque posterior nunca pueda alcanzarse.
La utilización de los argumentos catch es una manera de filtrar las excepciones que desea controlar. También
se puede usar una expresión de filtro que examine aún más la excepción para decidir si controlarla. Si la
expresión de filtro devuelve false, prosigue la búsqueda de un controlador.
Los filtros de excepción son preferibles para detectar y volver a producir (se explica a continuación) porque los
filtros dejan la pila intacta. Si un controlador posterior vuelca la pila, puede ver la procedencia original de la
excepción, más que solo la ubicación en la que se volvió a producir. Un uso común de las expresiones de filtro de
excepciones es el registro. Puede crear una función de filtro que siempre devuelva false y que también resulte
en un registro, o bien puede registrar excepciones a medida que se produzcan sin tener que controlarlas y
volver a generarlas.
Se puede usar una instrucción throw en un bloque catch para volver a iniciar la excepción detectada por la
instrucción catch . En el ejemplo siguiente se extrae información de origen de una excepción IOException y, a
continuación, se produce la excepción al método principal.
catch (FileNotFoundException e)
{
// FileNotFoundExceptions are handled here.
}
catch (IOException e)
{
// Extract some information from this exception, and then
// throw it to the parent method.
if (e.Source != null)
Console.WriteLine("IOException source: {0}", e.Source);
throw;
}
Puede capturar una excepción y producir una excepción diferente. Al hacerlo, especifique la excepción que
detectó como excepción interna, tal como se muestra en el ejemplo siguiente.
catch (InvalidCastException e)
{
// Perform some action here, and then throw a new exception.
throw new YourCustomException("Put your error message here.", e);
}
También se puede volver a producir una excepción sin una condición específica es true, tal y como se muestra en
el ejemplo siguiente.
catch (InvalidCastException e)
{
if (e.Data == null)
{
throw;
}
else
{
// Take some action.
}
}
NOTE
También es posible usar un filtro de excepción para obtener un resultado similar de una forma generalmente más limpia
(además de no modificar la pila, tal y como se explicó anteriormente en este documento). El ejemplo siguiente tiene un
comportamiento similar para los autores de llamada que el ejemplo anterior. La función inicia la excepción
InvalidCastException de vuelta al autor de la llamada cuando e.Data es null .
Desde dentro de un bloque try , solo deben inicializarse las variables que se declaran en el mismo. De lo
contrario, puede ocurrir una excepción antes de que se complete la ejecución del bloque. Por ejemplo, en el
siguiente ejemplo de código, la variable n se inicializa dentro del bloque try . Un intento de utilizar esta
variable fuera del bloque try en la instrucción Write(n) generará un error del compilador.
static void Main()
{
int n;
try
{
// Do not initialize this variable here.
n = 123;
}
catch
{
}
// Error: Use of unassigned local variable 'n'.
Console.Write(n);
}
Para obtener más información sobre la captura,vea try-catch-finally (try-catch-finally [Referencia de C#]).
Ejemplo
En el ejemplo siguiente, el bloque try contiene una llamada al método ProcessString que puede causar una
excepción. La cláusula catch contiene el controlador de excepciones que muestra un mensaje en la pantalla.
Cuando la instrucción throw se llama desde dentro ProcessString , el sistema busca la instrucción catch y
muestra el mensaje Exception caught .
class TryFinallyTest
{
static void ProcessString(string s)
{
if (s == null)
{
throw new ArgumentNullException(paramName: nameof(s), message: "parameter can't be null.");
}
}
try
{
ProcessString(s);
}
catch (Exception e)
{
Console.WriteLine("{0} Exception caught.", e);
}
}
}
/*
Output:
System.ArgumentNullException: Value cannot be null.
at TryFinallyTest.Main() Exception caught.
* */
Quite la marca de comentario de la línea throw new OperationCanceledException para ver lo que pasa cuando se
cancela un proceso asincrónico. La propiedad de la tarea IsCanceled se establece en true , y la excepción se
captura en el bloque catch . En algunas condiciones que no son aplicables a este ejemplo, la propiedad de la
tarea IsFaulted se establece en true y IsCanceled se establece en false .
public async Task DoSomethingAsync()
{
Task<string> theTask = DelayAsync();
try
{
string result = await theTask;
Debug.WriteLine("Result: " + result);
}
catch (Exception ex)
{
Debug.WriteLine("Exception Message: " + ex.Message);
}
Debug.WriteLine("Task IsCanceled: " + theTask.IsCanceled);
Debug.WriteLine("Task IsFaulted: " + theTask.IsFaulted);
if (theTask.Exception != null)
{
Debug.WriteLine("Task Exception Message: "
+ theTask.Exception.Message);
Debug.WriteLine("Task Inner Exception Message: "
+ theTask.Exception.InnerException.Message);
}
}
Ejemplo de Task.WhenAll
En el ejemplo siguiente se muestra el control de excepciones en el que varias tareas pueden producir varias
excepciones. El bloque try espera la tarea devuelta por una llamada a Task.WhenAll. La tarea se completa
cuando se hayan completado las tres tareas a las que se aplica el método WhenAll.
Cada una de las tres tareas produce una excepción. El bloque catch se itera a través de las excepciones, que se
encuentran en la propiedad Exception.InnerExceptions de la tarea devuelta por Task.WhenAll.
public async Task DoMultipleAsync()
{
Task theTask1 = ExcAsync(info: "First Task");
Task theTask2 = ExcAsync(info: "Second Task");
Task theTask3 = ExcAsync(info: "Third Task");
try
{
await allTasks;
}
catch (Exception ex)
{
Debug.WriteLine("Exception: " + ex.Message);
Debug.WriteLine("Task IsFaulted: " + allTasks.IsFaulted);
foreach (var inEx in allTasks.Exception.InnerExceptions)
{
Debug.WriteLine("Task Inner Exception: " + inEx.Message);
}
}
}
// Output:
// Exception: Error-First Task
// Task IsFaulted: True
// Task Inner Exception: Error-First Task
// Task Inner Exception: Error-Second Task
// Task Inner Exception: Error-Third Task
Vea también
Referencia de C#
Guía de programación de C#
Palabras clave de C#
Instrucciones try, throw y catch (C++)
throw
try-finally
Cómo: Iniciar excepciones explícitamente
try-finally (Referencia de C#)
16/09/2021 • 3 minutes to read
Mediante el uso de un bloque finally , puede limpiar todos los recursos asignados en un bloque try y ejecutar
código incluso si se produce una excepción en el bloque try . Normalmente, las instrucciones de un bloque
finally se ejecutan cuando el control abandona una instrucción try . La transferencia de control se puede
producir como resultado de la ejecución normal, de la ejecución de una instrucción break , continue , goto o
return , o de la propagación de una excepción fuera de la instrucción try .
Dentro de una excepción controlada, se garantiza la ejecución del bloque finally asociado. Pero en el caso de
una excepción no controlada, la ejecución del bloque finally depende de la manera en que se desencadene la
operación de desenredo de la excepción. Esto, a su vez, depende de cómo esté configurado el equipo. Los únicos
casos en los que no se ejecutan las cláusulas finally implican que un programa se detenga inmediatamente.
Un ejemplo de esto sería cuando se produce InvalidProgramException debido a que las instrucciones IL están
dañadas. En la mayoría de los sistemas operativos, la limpieza de recursos razonable tendrá lugar como parte
de la detención y descarga del proceso.
Normalmente, cuando una excepción no controlada finaliza una aplicación, no es importante si el bloque
finally se ejecuta o no. Pero si tiene instrucciones en un bloque finally que debe ejecutarse incluso en esa
situación, una solución es agregar un bloque catch a la instrucción try - finally . Como alternativa, puede
capturar la excepción que se podría producir en el bloque try de una instrucción try - finally más arriba en
la pila de llamadas. Es decir, puede capturar la excepción en el método que llama al método que contiene la
instrucción try - finally , en el método que llama a ese método o en cualquier método en la pila de llamadas.
Si no se captura la excepción, la ejecución del bloque finally depende de si el sistema operativo decide
desencadenar una operación de desenredo de la excepción.
Ejemplo
En el ejemplo siguiente, una instrucción de conversión no válida provoca una excepción
System.InvalidCastException . Se trata de una excepción no controlada.
public class ThrowTestA
{
public static void Main()
{
int i = 123;
string s = "Some string";
object obj = s;
try
{
// Invalid conversion; obj contains a string, not a numeric type.
i = (int)obj;
En el ejemplo siguiente, se captura una excepción del método TryCast en un método más arriba en la pila de
llamadas.
public class ThrowTestB
{
public static void Main()
{
try
{
// TryCast produces an unhandled exception.
TryCast();
}
catch (Exception ex)
{
// Catch the exception that is unhandled in TryCast.
Console.WriteLine
("Catching the {0} exception triggers the finally block.",
ex.GetType());
try
{
// Invalid conversion; obj contains a string, not a numeric type.
i = (int)obj;
Vea también
Referencia de C#
Guía de programación de C#
Palabras clave de C#
Instrucciones try, throw y catch (C++)
throw
try-catch
Cómo: Iniciar excepciones explícitamente
try-catch-finally (Referencia de C#)
16/09/2021 • 2 minutes to read
Un uso habitual de catch y finally juntos es obtener y usar recursos de un bloque try , lidiar con
circunstancias excepcionales de un bloque catch y liberar los recursos del bloque finally .
Para más información y ejemplos sobre cómo volver a iniciar excepciones, vea try-catch y Generación de
excepciones. Para más información sobre el bloque finally , vea try-finally.
Ejemplo
public class EHClass
{
void ReadFile(int index)
{
// To run this code, substitute a valid path from your local machine
string path = @"c:\users\public\test.txt";
System.IO.StreamReader file = new System.IO.StreamReader(path);
char[] buffer = new char[10];
try
{
file.ReadBlock(buffer, index, buffer.Length);
}
catch (System.IO.IOException e)
{
Console.WriteLine("Error reading from {0}. Message = {1}", path, e.Message);
}
finally
{
if (file != null)
{
file.Close();
}
}
// Do something with buffer...
}
}
Vea también
Referencia de C#
Guía de programación de C#
Palabras clave de C#
Instrucciones try, throw y catch (C++)
throw
Cómo: Iniciar excepciones explícitamente
using (instrucción)
Checked y Unchecked (Referencia de C#)
16/09/2021 • 2 minutes to read
Conversiones numéricas explícitas entre tipos integrales o de float o double a un tipo integral.
Si no se especifica checked ni unchecked , el contexto predeterminado para expresiones no constantes (las que
se evalúan en tiempo de ejecución) se define por medio del valor de la opción del compilador
CheckForOverflowUnderflow . De forma predeterminada, el valor de esa opción se desactiva y se ejecutan
operaciones aritméticas en un contexto sin comprobar.
Para expresiones constantes (expresiones que se pueden evaluar completamente en tiempo de compilación), el
contexto predeterminado se comprueba siempre. A menos que se coloque de forma explícita una expresión
constante en un contexto sin comprobar, los desbordamientos que se producen durante la evaluación de tiempo
de compilación de la expresión dan lugar a errores en tiempo de compilación.
Vea también
Referencia de C#
Guía de programación de C#
Palabras clave de C#
Palabras clave de instrucciones
checked (Referencia de C#)
16/09/2021 • 2 minutes to read
La palabra clave checked se usa con el fin de habilitar explícitamente la comprobación de desbordamiento para
operaciones aritméticas y conversiones de tipo integral.
De manera predeterminada, una expresión que solo contiene valores constantes provoca un error del
compilador si la expresión genera un valor fuera del intervalo del tipo de destino. Si la expresión contiene uno o
varios valores no constantes, el compilador no detecta el desbordamiento. En el siguiente ejemplo, la evaluación
de la expresión asignada a i2 no genera un error del compilador.
// The following example, which includes variable ten, does not cause
// a compiler error.
int ten = 10;
int i2 = 2147483647 + ten;
// Checked expression.
Console.WriteLine(checked(2147483647 + ten));
// Checked block.
checked
{
int i3 = 2147483647 + ten;
Console.WriteLine(i3);
}
Ejemplo
En este ejemplo, se muestra cómo usar checked para habilitar la comprobación de desbordamiento en tiempo
de ejecución.
class OverFlowTest
{
// Set maxIntValue to the maximum value for integers.
static int maxIntValue = 2147483647;
La palabra clave unchecked se usa para suprimir la comprobación de desbordamiento en las operaciones y
conversiones aritméticas de tipos enteros.
En un contexto unchecked, si una expresión genera un valor que está fuera del intervalo del tipo de destino, no
se marca el desbordamiento. Por ejemplo, dado que el cálculo del siguiente ejemplo se realiza en un bloque o
una expresión unchecked , el hecho de que el resultado sea demasiado grande para un entero se omite y se
asigna a int1 el valor -2 147 483 639.
unchecked
{
int1 = 2147483647 + 10;
}
int1 = unchecked(ConstantMax + 10);
Ejemplo
En este ejemplo se muestra cómo usar la palabra clave unchecked .
class UncheckedDemo
{
static void Main(string[] args)
{
// int.MaxValue is 2,147,483,647.
const int ConstantMax = int.MaxValue;
int int1;
int int2;
int variableMax = 2147483647;
// The following statements are checked by default at compile time. They do not
// compile.
//int1 = 2147483647 + 10;
//int1 = ConstantMax + 10;
// To enable the assignments to int1 to compile and run, place them inside
// an unchecked block or expression. The following statements compile and
// run.
unchecked
{
int1 = 2147483647 + 10;
}
int1 = unchecked(ConstantMax + 10);
// To catch the overflow in the assignment to int2 at run time, put the
// declaration in a checked block or expression. The following
// statements compile but raise an overflow exception at run time.
checked
{
//int2 = variableMax + 10;
}
//int2 = checked(variableMax + 10);
Vea también
Referencia de C#
Guía de programación de C#
Palabras clave de C#
Checked y unchecked
checked
fixed (Instrucción, Referencia de C#)
16/09/2021 • 3 minutes to read
La instrucción fixed evita que el recolector de elementos no utilizados reubique una variable móvil. La
instrucción fixed solo se permite en un contexto de unsafe. También puede usar la palabra clave fixed para
crear búferes de tamaño fijo.
La instrucción fixed establece un puntero a una variable administrada y "ancla" esa variable durante su
ejecución. Los punteros a variables administradas móviles solo son útiles en un contexto fixed . Sin un contexto
fixed , la recolección de elementos no utilizados podría reubicar las variables de forma impredecible. El
compilador de C# solo permite asignar un puntero a una variable administrada en una instrucción fixed .
class Point
{
public int x;
public int y;
}
Puede inicializar un puntero mediante una matriz, una cadena, un búfer de tamaño fijo o la dirección de una
variable. En el ejemplo siguiente se muestra el uso de direcciones, matrices y cadenas de variable:
// The following two assignments are equivalent. Each assigns the address
// of the first element in array arr to pointer p.
A partir de C# 7.3, la instrucción fixed funciona en tipos adicionales más allá de matrices, cadenas, búferes de
tamaño fijo o variables no administradas. Cualquier tipo que implemente un método denominado
GetPinnableReference se puede anclar. GetPinnableReference debe devolver una variable ref a un tipo no
administrado. Los tipos de .NET System.Span<T> y System.ReadOnlySpan<T> presentados en .NET Core 2.0
usan este patrón y se pueden anclar. Esto se muestra en el ejemplo siguiente:
Si crea tipos que deben participar en este patrón, consulte Span<T>.GetPinnableReference() para ver un
ejemplo de implementación del patrón.
Es posible inicializar varios punteros en una sola instrucción si todos son del mismo tipo:
Para inicializar punteros de tipos diferentes, simplemente anide instrucciones fixed , como se muestra en el
ejemplo siguiente.
Después de ejecutar el código de la instrucción, las variables ancladas se desanclan y quedan sujetas a la
recolección de elementos no utilizados. Por lo tanto, no debe apuntar a esas variables fuera de la instrucción
fixed . Las variables declaradas en la instrucción fixed se limitan a dicha instrucción, lo que simplifica esta
tarea:
Puede asignar memoria en la pila, donde no está sujeta a la recolección de elementos no utilizados y, por tanto,
no necesita anclarse. Para ello, use una expresión stackalloc .
Vea también
Referencia de C#
Guía de programación de C#
Palabras clave de C#
unsafe
Tipos de puntero
Búferes de tamaño fijo
Instrucción lock (Referencia de C#)
16/09/2021 • 2 minutes to read
La instrucción lock adquiere el bloqueo de exclusión mutua de un objeto determinado, ejecuta un bloque de
instrucciones y luego libera el bloqueo. Mientras se mantiene un bloqueo, el subproceso que lo mantiene puede
volver a adquirir y liberar el bloqueo. Ningún otro subproceso puede adquirir el bloqueo y espera hasta que se
libera.
La instrucción lock tiene el formato
lock (x)
{
// Your code...
}
object __lockObj = x;
bool __lockWasTaken = false;
try
{
System.Threading.Monitor.Enter(__lockObj, ref __lockWasTaken);
// Your code...
}
finally
{
if (__lockWasTaken) System.Threading.Monitor.Exit(__lockObj);
}
Puesto que el código usa un bloque try... finally, el bloqueo se libera aunque se produzca una excepción dentro
del cuerpo de una instrucción lock .
No se puede usar el operador await en el cuerpo de una instrucción lock .
Instrucciones
Al sincronizar el acceso del subproceso al recurso compartido, bloquee una instancia dedicada de objeto (por
ejemplo, private readonly object balanceLock = new object(); ) u otra instancia cuyo empleo como objeto de
bloqueo sea poco probable por parte de elementos no relacionados del código. Evite el uso de la misma
instancia de objeto de bloqueo para distintos recursos compartidos, ya que se podría producir un interbloqueo
o una contención de bloqueo. En particular, evite utilizar lo siguiente como objetos de bloqueo:
this , porque los autores de llamadas podrían usarlo como un bloqueo.
Instancias Type, porque el operador o la reflexión typeof podrían obtenerlas.
Instancias de cadena, incluidos literales de cadena, porque podrían internarse.
Mantenga el bloqueo durante el menor tiempo posible para reducir la contención de bloqueo.
Ejemplo
En el ejemplo siguiente se define una clase Account que sincroniza el acceso a su campo privado balance
mediante el bloqueo de una instancia dedicada balanceLock . El empleo de la misma instancia para bloquear
garantiza que el campo balance no sea actualizado al mismo tiempo por dos subprocesos que intentan llamar
a los métodos Debit o Credit simultáneamente.
using System;
using System.Threading.Tasks;
decimal appliedAmount = 0;
lock (balanceLock)
{
if (balance >= amount)
{
balance -= amount;
appliedAmount = amount;
}
}
return appliedAmount;
}
lock (balanceLock)
{
balance += amount;
}
}
class AccountTest
{
static async Task Main()
{
var account = new Account(1000);
var tasks = new Task[100];
for (int i = 0; i < tasks.Length; i++)
{
tasks[i] = Task.Run(() => Update(account));
}
await Task.WhenAll(tasks);
Console.WriteLine($"Account's balance is {account.GetBalance()}");
// Output:
// Account's balance is 2000
// Account's balance is 2000
}
Vea también
Referencia de C#
Palabras clave de C#
System.Threading.Monitor
System.Threading.SpinLock
System.Threading.Interlocked
Información general sobre las primitivas de sincronización
Parámetros de métodos (Referencia de C#)
16/09/2021 • 2 minutes to read
Los parámetros declarados para un método sin in, ref o out se pasan al método llamado por valor. Ese valor se
puede cambiar en el método, pero el cambio se perderá cuando se devuelva el control al procedimiento que ha
realizado la llamada. Si usa palabras clave de parámetros de método en la declaración del parámetro, puede
modificar este comportamiento.
Esta sección describe las palabras clave que puede usar para declarar parámetros de métodos:
params especifica que este parámetro puede tomar un número variable de argumentos.
in especifica que este parámetro se pasa por referencia, pero solo se lee mediante el método llamado.
ref especifica que este parámetro se pasa por referencia y puede ser leído o escrito por el método
llamado.
out especifica que este parámetro se pasa por referencia y se escribe mediante el método llamado.
Vea también
Referencia de C#
Guía de programación de C#
Palabras clave de C#
params (Referencia de C#)
16/09/2021 • 2 minutes to read
Mediante el uso de la palabra clave params , puede especificar un parámetro de método que toma un número
variable de argumentos. El tipo de parámetro debe ser una matriz unidimensional.
No se permiten parámetros adicionales después de la palabra clave params en una declaración de método, y
solo se permite una palabra clave params en una declaración de método.
Si el tipo declarado del parámetro params no es una matriz unidimensional, se produce el error CS0225 del
compilador.
Cuando se llama a un método con un parámetro params , se puede pasar:
Una lista separada por comas de argumentos del tipo de los elementos de la matriz.
Una matriz de argumentos del tipo especificado.
Sin argumentos. Si no envía ningún argumento, la longitud de la lista params es cero.
Ejemplo
En el ejemplo siguiente se muestran varias maneras de enviar argumentos a un parámetro params .
public class MyClass
{
public static void UseParams(params int[] list)
{
for (int i = 0; i < list.Length; i++)
{
Console.Write(list[i] + " ");
}
Console.WriteLine();
}
// The following call does not cause an error, but the entire
// integer array becomes the first element of the params array.
UseParams2(myIntArray);
}
}
/*
Output:
1 2 3 4
1 a test
5 6 7 8 9
2 b test again
System.Int32[]
*/
Vea también
Referencia de C#
Guía de programación de C#
Palabras clave de C#
Parámetros de métodos
Modificador del parámetro in (referencia de C#)
16/09/2021 • 6 minutes to read
La palabra clave in hace que los argumentos se pasen por referencia pero garantiza que el argumento no se
modifica. Hace que el parámetro formal sea un alias para el argumento, que debe ser una variable. En otras
palabras, cualquier operación en el parámetro se realiza en el argumento. Es como las palabras clave ref o out,
salvo que el método al que se llama no puede modificar los argumentos in . Mientras que los argumentos ref
se pueden modificar, el método llamado debe modificar los argumentos out y esas modificaciones se pueden
observar en el contexto de la llamada.
En el ejemplo anterior se muestra que el modificador in no suele ser necesario en el sitio de llamada, sino que
solo lo es en la declaración del método.
NOTE
Además, la palabra clave in puede usarse con un parámetro de tipo genérico para especificar que el parámetro de tipo
es contravariante, parte de una instrucción foreach o de una cláusula join de una consulta de LINQ. Para más
información sobre el uso de la palabra clave in en esos contextos, vea in, que además incluye vínculos a todos estos
usos.
Las variables que se han pasado como argumentos in deben inicializarse antes de pasarse en una llamada de
método. Sin embargo, es posible que el método llamado no asigne ningún valor o modifique el argumento.
El modificador de parámetro in está disponible en C# 7.2 y versiones posteriores. Las versiones anteriores
generan el error del compilador CS8107 ("Feature 'readonly references' is not available in C# 7.0. Please use
language version 7.2 or greater.") (La característica "readonly references" no está disponible en C# 7.0. Use la
versión de lenguaje 7.2 o superior"). Para configurar la versión del lenguaje de compilador, vea Seleccionar la
versión del lenguaje C#.
Aunque los modificadores de parámetro in , out, y ref se consideran parte de una firma, los miembros
declarados en un único tipo no pueden diferir en la firma únicamente por in , ref y out . Por lo tanto, los
métodos no pueden sobrecargarse si la única diferencia es que un método toma un argumento ref o in y el
otro toma un argumento out . Por ejemplo, el código siguiente, no se compilará:
class CS0663_Example
{
// Compiler error CS0663: "Cannot define overloaded
// methods that differ only on in, ref and out".
public void SampleMethod(in int i) { }
public void SampleMethod(ref int i) { }
}
Está permitida la sobrecarga en función de la presencia de in :
class InOverloads
{
public void SampleMethod(in int i) { }
public void SampleMethod(int i) { }
}
Supongamos ahora que hay disponible otro método que usa argumentos por valor. Los resultados cambian
como se muestra en este código:
NOTE
El código anterior usa int como el tipo de argumento para simplificar el trabajo. Como int no es más grande que una
referencia en la mayoría de máquinas modernas, no supone ninguna ventaja pasar un único int como una referencia de
solo lectura.
La palabra clave ref indica un valor que se ha pasado por referencia. Se usa en cuatro contextos diferentes:
En una firma del método y en una llamada al método, para pasar un argumento a un método mediante
referencia. Para más información, vea Pasar un argumento mediante referencia.
En una firma del método, para devolver un valor al autor de la llamada mediante referencia. Para obtener
más información, consulte Valores devueltos de referencia.
En un cuerpo de miembro, para indicar que un valor devuelto de referencia se almacena localmente como
una referencia que el autor de la llamada pretende modificar. O para indicar que una variable local tiene
acceso a otro valor por referencia. Para más información, vea Variables locales de tipo ref.
En una declaración struct , para declarar ref struct o readonly ref struct . Para obtener más información,
vea la sección struct ref del artículo tipos de estructura.
NOTE
No confunda el concepto de pasar por referencia con el concepto de tipos de referencia. Estos dos conceptos no son lo
mismo. Un parámetro de método puede ser modificado por ref independientemente de si se trata de un tipo de valor
o de un tipo de referencia. No hay ninguna conversión boxing de un tipo de valor cuando se pasa por referencia.
Para usar un parámetro ref , la definición de método y el método de llamada deben utilizar explícitamente la
palabra clave ref , como se muestra en el ejemplo siguiente. (Salvo que el método de llamada puede omitir
ref al realizar una llamada COM).
int number = 1;
Method(ref number);
Console.WriteLine(number);
// Output: 45
Un argumento que se pasa a un parámetro ref o in debe inicializarse antes de pasarlo. Este requisito difiere
de los parámetros out, cuyos argumentos no tienen que inicializarse explícitamente antes de pasarlos.
Los miembros de una clase no pueden tener signaturas que se diferencien solo por ref , in o out . Si la única
diferencia entre dos miembros de un tipo es que uno de ellos tiene un parámetro ref y el otro tiene un
parámetro out o in , se produce un error de compilador. El código siguiente, por ejemplo, no se compila.
class CS0663_Example
{
// Compiler error CS0663: "Cannot define overloaded
// methods that differ only on ref and out".
public void SampleMethod(out int i) { }
public void SampleMethod(ref int i) { }
}
En cambio, los métodos pueden sobrecargarse cuando un método tiene un parámetro ref , in o out y el otro
tiene un parámetro que se pasa por valor, como se muestra en el ejemplo siguiente.
class RefOverloadExample
{
public void SampleMethod(int i) { }
public void SampleMethod(ref int i) { }
}
En otras situaciones que requieran firma coincidente, como ocultar o reemplazar, in , ref y out forman parte
de la signatura y no coinciden entre sí.
Las propiedades no son variables. Son métodos y no se pueden pasar a parámetros ref .
Las palabras clave ref , in y out no pueden usarse para estos tipos de métodos:
Métodos asincrónicos, que se definen mediante el uso del modificador async.
Métodos de iterador, que incluyen una instrucción yield return o yield break .
Los métodos de extensión también tienen restricciones en el uso de estas palabras clave:
No se puede usar la palabra clave out en el primer argumento de un método de extensión.
No se puede usar la palabra clave ref en el primer argumento de un método de extensión cuando el
argumento no es un struct ni un tipo genérico no restringido a ser un struct.
No se puede usar la palabra clave in a menos que el primer argumento sea un struct. No se puede usar la
palabra clave in en ningún tipo genérico, incluso cuando está restringido a ser un struct.
Para obtener más información sobre cómo pasar tipos de referencia por valor y por referencia, vea Pasar
parámetros Reference-Type .
Entre el token return y la variable devuelta en una instrucción return en el método. Por ejemplo:
El método llamado también puede declarar el valor devuelto como ref readonly para devolver el valor por
referencia y exigir que el código de llamada no pueda modificar el valor devuelto. El método de llamada puede
evitar copiar el valor devuelto si lo almacena en una variable de tipo ref readonly.
Para obtener un ejemplo, vea Un ejemplo de valores devueltos y variables locales de tipo ref.
Puede acceder a un valor por referencia de la misma manera. En algunos casos, acceder a un valor por
referencia aumenta el rendimiento, ya que evita una operación de copia potencialmente cara. Por ejemplo, en la
instrucción siguiente se muestra cómo definir una variable local de referencia que se usa para hacer referencia a
un valor.
En ambos ejemplos la palabra clave ref debe usarse en ambos lugares. De lo contrario, el compilador genera
el error CS8172, "No se puede inicializar una variable por referencia con un valor".
A partir C# 7.3, la variable de iteración de la instrucción foreach puede ser una variable ref local o ref readonly
local. Para más información, vea el artículo sobre la instrucción foreach.
A partir C# 7.3, las variables locales de tipo ref o locales de tipo ref readonly se pueden reasignar con el
operador de asignación ref.
Cuando el autor de la llamada almacena el valor devuelto mediante el método GetBookByTitle como una
variable local de tipo ref, los cambios que el autor de la llamada realiza en el valor devuelto se reflejan en el
objeto BookCollection , como se muestra en el ejemplo siguiente.
var bc = new BookCollection();
bc.ListBooks();
Vea también
Escritura de código seguro y eficaz
Devoluciones y variables locales ref
Expresión condicional ref
Pasar parámetros
Parámetros de métodos
Referencia de C#
Guía de programación de C#
Palabras clave de C#
Modificador del parámetro out (Referencia de C#)
16/09/2021 • 4 minutes to read
La palabra clave out hace que los argumentos se pasen por referencia. Hace que el parámetro formal sea un
alias para el argumento, que debe ser una variable. En otras palabras, cualquier operación en el parámetro se
realiza en el argumento. Esto es como la palabra clave ref, salvo que ref requiere que se inicialice la variable
antes de pasarla. También es como la palabra clave in, salvo que in no permite que el método llamado
modifique el valor del argumento. Para usar un parámetro out , tanto la definición de método como el método
de llamada deben utilizar explícitamente la palabra clave out . Por ejemplo:
int initializeInMethod;
OutArgExample(out initializeInMethod);
Console.WriteLine(initializeInMethod); // value is now 44
NOTE
La palabra clave out también puede usarse con un parámetro de tipo genérico para especificar que el parámetro de tipo
es covariante. Para obtener más información sobre el uso de la palabra clave out en este contexto, vea Out (Modificador
genérico).
Las variables que se han pasado como argumentos out no tienen que inicializarse antes de pasarse en una
llamada al método. En cambio, se necesita el método que se ha llamado para asignar un valor antes de que el
método se devuelva.
Las palabras clave in , ref y out no se consideran parte de la firma del método con el fin de resolver la
sobrecarga. Por lo tanto, los métodos no pueden sobrecargarse si la única diferencia es que un método toma un
argumento ref o in y el otro toma un argumento out . Por ejemplo, el código siguiente, no se compilará:
class CS0663_Example
{
// Compiler error CS0663: "Cannot define overloaded
// methods that differ only on ref and out".
public void SampleMethod(out int i) { }
public void SampleMethod(ref int i) { }
}
En cambio, la sobrecarga es legal si un método toma un argumento ref , in o out y el otro no tiene ninguno
de estos modificadores, como se muestra aquí:
class OutOverloadExample
{
public void SampleMethod(int i) { }
public void SampleMethod(out int i) => i = 5;
}
El compilador elige la mejor sobrecarga haciendo coincidir los modificadores de parámetro del sitio de llamada
con los usados en la llamada de método.
Las propiedades no son variables y, por tanto, no pueden pasarse como parámetros out .
Las palabras clave in , ref y out no pueden usarse para estos tipos de métodos:
Métodos asincrónicos, que se definen mediante el uso del modificador async.
Métodos de iterador, que incluyen una instrucción yield return o yield break .
Además, los métodos de extensión tienen las restricciones siguientes:
No se puede usar la palabra clave out en el primer argumento de un método de extensión.
No se puede usar la palabra clave ref en el primer argumento de un método de extensión cuando el
argumento no es un struct ni un tipo genérico no restringido a ser un struct.
No se puede usar la palabra clave in a menos que el primer argumento sea un struct. No se puede usar la
palabra clave in en ningún tipo genérico, incluso cuando está restringido a ser un struct.
void Method(out int answer, out string message, out string stillNull)
{
answer = 44;
message = "I've been returned";
stillNull = null;
}
int argNumber;
string argMessage, argDefault;
Method(out argNumber, out argMessage, out argDefault);
Console.WriteLine(argNumber);
Console.WriteLine(argMessage);
Console.WriteLine(argDefault == null);
int number;
if (Int32.TryParse(numberAsString, out number))
Console.WriteLine($"Converted '{numberAsString}' to {number}");
else
Console.WriteLine($"Unable to convert '{numberAsString}'");
// The example displays the following output:
// Converted '1640' to 1640
A partir de C# 7.0, puede declarar la variable out en la lista de argumentos de la llamada al método, en lugar
de en una declaración de variable independiente. Esto genera un código legible más compacto y, además, evita
que asigne un valor a la variable antes de la llamada al método de manera involuntaria. El ejemplo siguiente es
como el ejemplo anterior, excepto que define la variable number en la llamada al método Int32.TryParse.
En el ejemplo anterior, la variable number está fuertemente tipada como int . También puede declarar una
variable local con tipo implícito como se muestra en el siguiente ejemplo.
Vea también
Referencia de C#
Guía de programación de C#
Palabras clave de C#
Parámetros de métodos
namespace
16/09/2021 • 2 minutes to read
La palabra clave namespace se usa para declarar un ámbito que contiene un conjunto de objetos relacionados.
Puede usar un espacio de nombres para organizar los elementos de código y crear tipos únicos globales.
namespace SampleNamespace
{
class SampleClass { }
interface ISampleInterface { }
struct SampleStruct { }
enum SampleEnum { a, b }
namespace Nested
{
class SampleClass2 { }
}
}
Las declaraciones de espacio de nombres con ámbito de archivo permiten declarar que todos los tipos de un
archivo están en un único espacio de nombres. Las declaraciones de espacio de nombres con ámbito de archivo
están disponibles con C# 10.0. El ejemplo siguiente es similar al anterior, pero usa una declaración de espacio de
nombres con ámbito de archivo:
using System;
namespace SampleFileScopedNamespace;
class SampleClass { }
interface ISampleInterface { }
struct SampleStruct { }
enum SampleEnum { a, b }
En el ejemplo anterior no se incluye ningún espacio de nombres anidado. Los espacios de nombres con ámbito
de archivo no pueden incluir declaraciones de espacio de nombres adicionales. No se puede declarar un espacio
de nombres anidado ni un segundo espacio de nombres con ámbito de archivo:
namespace SampleNamespace;
class AnotherSampleClass
{
public void AnotherSampleMethod()
{
System.Console.WriteLine(
"SampleMethod inside SampleNamespace");
}
}
namespace MyCompany.Proj1
{
class MyClass
{
}
}
namespace MyCompany.Proj1
{
class MyClass1
{
}
}
En el ejemplo siguiente se muestra cómo llamar a un método estático en un espacio de nombres anidado.
namespace SomeNameSpace
{
public class MyClass
{
static void Main()
{
Nested.NestedNameSpaceClass.SayHello();
}
}
// a nested namespace
namespace Nested
{
public class NestedNameSpaceClass
{
public static void SayHello()
{
Console.WriteLine("Hello");
}
}
}
}
// Output: Hello
Vea también
Referencia de C#
Palabras clave de C#
using
using static
Calificadores de alias de espacio de nombres ::
Espacios de nombres
using (Referencia de C#)
16/09/2021 • 2 minutes to read
Vea también
Referencia de C#
Guía de programación de C#
Palabras clave de C#
Espacios de nombres
extern
using (directiva)
16/09/2021 • 9 minutes to read
La directiva using permite usar tipos definidos en un espacio de nombres sin especificar el espacio de nombres
completo de ese tipo. En su forma básica, la directiva using importa todos los tipos de un único espacio de
nombres, como se muestra en el ejemplo siguiente:
using System.Text;
NOTE
La palabra clave using también se usa para crear instrucciones using, que ayudan a garantizar que los objetos
IDisposable, como archivos y fuentes, se tratan correctamente. Para obtener más información sobre la instrucción using,
consulte Instrucción using.
El ámbito de una directiva using sin el modificador global es el archivo en el que aparece.
La directiva using puede aparecer:
Al principio de un archivo de código fuente, antes de las declaraciones de espacio de nombres o de tipo.
En cualquier espacio de nombres, pero antes de los espacios de nombres o los tipos declarados en ese
espacio de nombres, a menos que se use el modificador global , en cuyo caso la directiva debe aparecer
antes de todas las declaraciones de tipos y espacios de nombres.
De lo contrario, se generará el error de compilador CS1529.
Cree una directiva using para usar los tipos de un espacio de nombres sin tener que especificarlo. Una directiva
using no proporciona acceso a los espacios de nombres que están anidados en el espacio de nombres
especificado. Los espacios de nombres se dividen en dos categorías: definidos por el sistema y definidos por el
usuario. Los espacios de nombres definidos por el usuario son espacios de nombres definidos en el código. Para
obtener una lista de los espacios de nombres definidos por el sistema, vea Explorador de API de .NET.
modificador global
Agregar el modificador global a una directiva using hará que using se aplique a todos los archivos de la
compilación (normalmente un proyecto). La directiva global using se agregó en C# 10.0. La sintaxis es:
donde el espacio de nombres completo es el nombre completo del espacio de nombres cuyos tipos se pueden
hacer referencia sin especificar el espacio de nombres.
Una directiva global using puede aparecer al principio de cualquier archivo de código fuente. Todas las directivas
global using de un solo archivo deben aparecer antes de:
IMPORTANT
Las plantillas de C# para .NET 6 usan instrucciones de nivel superior. Es posible que la aplicación no coincida con el código
de este artículo si ya ha actualizado a las versiones preliminares de .NET 6. Para obtener más información, consulte el
artículo Las nuevas plantillas de C# generan instrucciones de nivel superior.
El SDK de .NET 6 también agrega un conjunto de directivas de global using implícitas para proyectos que usan los SDK
siguientes:
Microsoft.NET.Sdk
Microsoft.NET.Sdk.Web
Microsoft.NET.Sdk.Worker
Estas directivas de global using implícitas incluyen los espacios de nombres más comunes para el tipo de proyecto.
static (modificador)
La directiva using static designa un tipo a cuyos miembros estáticos y tipos anidados se puede acceder sin
especificar un nombre de tipo. La directiva using static se ha agregado en C# 6. La sintaxis es:
<fully-qualified-type-name> es el nombre del tipo a cuyos miembros estáticos y tipos anidados se puede hacer
referencia sin especificar un nombre de tipo. Si no proporciona un nombre de tipo completo (el nombre del
espacio de nombres completo junto con el nombre del tipo), C# genera el error del compilador CS0246: "El
nombre del tipo o del espacio de nombres "tipo/espacio de nombres" no se encontró (¿falta una directiva using
o una referencia de ensamblado?)".
La directiva using static se aplica a cualquier tipo que tenga miembros estáticos (o tipos anidados), aunque
también tenga miembros de instancia. Pero los miembros de instancia solo pueden invocarse a través de la
instancia del tipo.
Puede acceder a los miembros estáticos de un tipo sin tener que calificar el acceso con el nombre del tipo:
Normalmente, cuando se llama a un miembro estático, se especifica el nombre de tipo junto con el nombre de
miembro. Especificar varias veces el mismo nombre de tipo para invocar los miembros del tipo puede traducirse
en código detallado y poco claro. Por ejemplo, en la definición siguiente de una clase Circle se hace referencia
a numerosos miembros de la clase Math.
using System;
Al eliminar la necesidad de hacer referencia explícitamente a la clase Math cada vez que se hace referencia a un
miembro, la directiva using static genera un código más limpio:
using System;
using static System.Math;
using static importa solo los miembros estáticos accesibles y los tipos anidados declarados en el tipo
especificado. Los miembros heredados no se importan. Puede realizar la importación desde cualquier tipo con
nombre con una directiva using static , como los módulos de Visual Basic. Las funciones de nivel superior F#
pueden importarse si aparecen en los metadatos como miembros estáticos de un tipo con nombre cuyo
nombre es un identificador de C# válido.
using static habilita los métodos de extensión declarados en el tipo especificado estén para la búsqueda de
métodos de extensión. Sin embargo, los nombres de los métodos de extensión no se importan en el ámbito de
referencia sin calificar del código.
Los métodos con el mismo nombre que se importen desde tipos distintos con distintas directivas using static
en la misma unidad de compilación o espacio de nombres forman un grupo de métodos. La resolución de
sobrecarga dentro de estos grupos de método sigue reglas normales de C#.
En el ejemplo siguiente se usa la directiva using static para que los miembros estáticos de las clases Console,
Math y String estén disponibles sin tener que especificar su nombre de tipo.
using System;
using static System.Console;
using static System.Math;
using static System.String;
class Program
{
static void Main()
{
Write("Enter a circle's radius: ");
var input = ReadLine();
if (!IsNullOrEmpty(input) && double.TryParse(input, out var radius)) {
var c = new Circle(radius);
En el ejemplo, también podría haberse aplicado la directiva using static al tipo Double. Agregar esa directiva
habría permitido llamar al método TryParse(String, Double) sin especificar un nombre de tipo. Sin embargo, el
uso de TryParse sin un nombre de tipo crea un código menos legible, ya que es necesario comprobar las
directivas using static para determinar el método TryParse del tipo numérico al que se llama.
alias using
Cree una directiva de alias using para facilitar la calificación de un identificador como espacio de nombres o
tipo. En cualquier directiva using , hay que usar el espacio de nombres o el tipo con cualificación completa,
independientemente de las directivas using que los precedan. No se puede usar ningún alias using en la
declaración de una directiva using . Por ejemplo, en el ejemplo siguiente se genera un error del compilador:
using s = System.Text;
using s.RegularExpressions; // Generates a compiler error.
En el ejemplo siguiente se muestra cómo definir y usar un alias using para un espacio de nombres:
namespace PC
{
// Define an alias for the nested namespace.
using Project = PC.MyCompany.Project;
class A
{
void M()
{
// Use the alias
var mc = new Project.MyClass();
}
}
namespace MyCompany
{
namespace Project
{
public class MyClass { }
}
}
}
Una directiva de alias using no puede tener un tipo genérico abierto en el lado derecho. Por ejemplo, no puede
crear un alias using para un elemento List<T> , pero puede crear uno para un elemento List<int> .
En el ejemplo siguiente se muestra cómo definir una directiva using y un alias using para una clase:
using System;
namespace NameSpace1
{
public class MyClass
{
public override string ToString()
{
return "You are in NameSpace1.MyClass.";
}
}
}
namespace NameSpace2
{
class MyClass<T>
{
public override string ToString()
{
return "You are in NameSpace2.MyClass.";
}
}
}
namespace NameSpace3
{
class MainClass
{
static void Main()
{
var instance1 = new AliasToMyClass();
Console.WriteLine(instance1);
Vea también
Referencia de C#
Guía de programación de C#
Utilizar espacios de nombres
Palabras clave de C#
Espacios de nombres
using (instrucción)
using (Instrucción, Referencia de C#)
16/09/2021 • 4 minutes to read
Ofrece una sintaxis adecuada que garantiza el uso correcto de objetos IDisposable. A partir de C# 8.0, la
instrucción using garantiza el uso correcto de los objetos IAsyncDisposable.
Ejemplo
En el ejemplo siguiente se muestra cómo usar la instrucción using .
A partir de C# 8.0, puede usar la siguiente sintaxis alternativa para la instrucción using que no requiere llaves:
Comentarios
File y Font son ejemplos de tipos administrados que acceden a recursos no administrados (en este caso,
identificadores de archivo y contextos de dispositivo). Hay muchos otros tipos de recursos no administrados y
tipos de la biblioteca de clases que los encapsulan. Todos estos tipos deben implementar la interfaz IDisposable
o la interfaz IAsyncDisposable.
Cuando la duración de un objeto IDisposable se limita a un único método, debe declarar y crear instancias del
mismo en la instrucción using . La instrucción using llama al método Dispose del objeto de forma correcta y
(cuando se usa tal y como se muestra anteriormente) también hace que el propio objeto salga del ámbito en
cuanto se llame a Dispose. Dentro del bloque using , el objeto es de solo lectura y no se puede modificar ni
reasignar. Si el objeto implementa IAsyncDisposable en lugar de IDisposable , la instrucción using llama al
objeto DisposeAsync y awaits al objeto ValueTask devuelto. Para obtener más información sobre
IAsyncDisposable, vea Implementación de un método DisposeAsync.
La instrucción using asegura que se llama al método Dispose (o DisposeAsync) aunque se produzca una
excepción en el bloque using . Puede obtener el mismo resultado si coloca el objeto dentro de un bloque try y
llama a Dispose (o DisposeAsync) en un bloque finally ; de hecho, es así como el compilador traduce la
instrucción using . El ejemplo de código anterior se extiende al siguiente código en tiempo de compilación
(tenga en cuenta las llaves adicionales para crear el ámbito limitado del objeto):
{
var reader = new StringReader(manyLines);
try {
string? item;
do {
item = reader.ReadLine();
Console.WriteLine(item);
} while(item != null);
} finally
{
reader?.Dispose();
}
}
La nueva sintaxis de la instrucción using se traduce en un código similar. Se abre el bloque try en el que se
declara la variable. El bloque finally se agrega al cierre del bloque de inclusión, normalmente, al final de un
método.
Consulte el artículo sobre try-finally para obtener más información sobre la instrucción try - finally .
Se pueden declarar varias instancias de un tipo en una sola instrucción using , tal y como se muestra en el
ejemplo siguiente. Tenga en cuenta que no se pueden usar variables con tipo implícito ( var ) cuando se
declaran varias variables en una sola instrucción:
string numbers=@"One
Two
Three
Four.";
string letters=@"A
B
C
D.";
También puede combinar varias declaraciones del mismo tipo con la nueva sintaxis introducida con C# 8, tal y
como se muestra en el ejemplo siguiente:
string numbers=@"One
Two
Three
Four.";
string letters=@"A
B
C
D.";
Puede crear una instancia del objeto de recurso y luego pasar la variable a la instrucción using , pero esto no es
un procedimiento recomendado. En este caso, después de que el control abandone el bloque using el objeto
permanece en el ámbito, pero probablemente ya no tenga acceso a sus recursos no administrados. En otras
palabras, ya no se inicializa totalmente. Si intenta usar el objeto fuera del bloque using , corre el riesgo de
iniciar una excepción. Por este motivo, es mejor crear una instancia del objeto en la instrucción using y limitar
su ámbito al bloque using .
Para obtener más información sobre cómo eliminar objetos IDisposable , vea Uso de objetos que implementan
IDisposable.
Vea también
Referencia de C#
Guía de programación de C#
Palabras clave de C#
using (directiva)
Recolección de elementos no utilizados
Uso de objetos que implementan IDisposable
IDisposable interface (Interfaz IDisposable)
Instrucción using en C# 8.0
alias externo (Referencia de C#)
16/09/2021 • 2 minutes to read
Es posible que deba hacer referencia a dos versiones de ensamblados que tienen los mismos nombres de tipo
completos. Por ejemplo, es posible que tenga que usar dos o más versiones de un ensamblado en la misma
aplicación. Mediante el uso de un alias de ensamblado externo, los espacios de nombres de cada ensamblado
pueden ajustarse en espacios de nombres de nivel de raíz denominados por el alias, lo que permite que se usen
en el mismo archivo.
NOTE
La palabra clave extern también se usa como un modificador de método, y declara un método escrito en código no
administrado.
Para hacer referencia a dos ensamblados con los mismos nombres de tipo completos, debe especificarse un
alias en un símbolo del sistema, como sigue:
/r:GridV1=grid.dll
/r:GridV2=grid20.dll
Esto crea los alias externos GridV1 y GridV2 . Para usar estos alias desde dentro de un programa, se hace
referencia a ellos mediante la palabra clave extern . Por ejemplo:
extern alias GridV1;
Cada declaración de alias externo introduce un espacio de nombres de nivel de raíz adicional que es semejante
al espacio de nombres global (pero que no se encuentra en su interior). Por tanto, se puede hacer referencia a
los tipos de cada ensamblado sin ambigüedad mediante su nombre completo, con raíz en el alias de espacio de
nombres adecuado.
En el ejemplo anterior, GridV1::Grid sería el control de cuadrícula de grid.dll ,y GridV2::Grid sería el control
de cuadrícula de grid20.dll .
Ahora puede crear un alias para un espacio de nombres o un tipo mediante la directiva de alias. Para más
información, consulte la directiva using.
using Class1V1 = GridV1::Namespace.Class1;
Vea también
Referencia de C#
Guía de programación de C#
Palabras clave de C#
:: !
References (opciones del compilador de C#)
Restricción new (Referencia de C#)
16/09/2021 • 2 minutes to read
La restricción new especifica que un tipo de argumento en una declaración de clase genérica debe tener un
constructor sin parámetros público. Para usar la restricción new , el tipo no puede ser abstracto.
Aplique la restricción new a un tipo de parámetro cuando una clase genérica cree otras instancias del tipo, tal y
como se muestra en el ejemplo siguiente:
Cuando use la restricción new() con otras restricciones, se debe especificar en último lugar:
Vea también
Referencia de C#
Guía de programación de C#
Palabras clave de C#
Genéricos
where (restricción de tipo genérico) (Referencia de
C#)
16/09/2021 • 5 minutes to read
La cláusula where en una definición genérica especifica restricciones en los tipos que se usan como argumentos
para los parámetros de tipo en un tipo genérico, método, delegado o función local. Las restricciones pueden
especificar interfaces o clases base, o bien requerir que un tipo genérico sea una referencia, un valor o un tipo
no administrado. Declaran las funcionalidades que debe tener el argumento de tipo.
Por ejemplo, se puede declarar una clase genérica, AGenericClass , de modo que el parámetro de tipo T
implemente la interfaz IComparable<T>:
NOTE
Para obtener más información sobre la cláusula where en una expresión de consulta, vea where (Cláusula).
La cláusula where también puede incluir una restricción de clase base. La restricción de clase base indica que un
tipo que se va a usar como argumento de tipo para ese tipo genérico tiene la clase especificada como clase base,
o bien es la clase base. Si se usa la restricción de clase base, debe aparecer antes que cualquier otra restricción
de ese parámetro de tipo. Algunos tipos no están permitidos como restricción de clase base: Object, Array y
ValueType. Antes de C# 7.3, tampoco se permitían Enum, Delegate ni MulticastDelegate como restricciones de
clase base. En el ejemplo siguiente se muestran los tipos que ahora se pueden especificar como clase base:
En un contexto que admite un valor NULL en C# 8.0 y versiones posteriores, se aplica la nulabilidad del tipo de
clase base. Si la clase base no acepta valores NULL (por ejemplo, Base ), el argumento de tipo no debe aceptar
valores NULL. Si la clase base admite un valor NULL (por ejemplo, Base? ), el argumento de tipo puede ser un
tipo de referencia que acepte o no valores NULL. El compilador emite una advertencia si el argumento de tipo es
un tipo de referencia que admite un valor NULL cuando la clase base no acepta valores NULL.
La cláusula where puede especificar que el tipo es class o struct . La restricción struct elimina la necesidad
de especificar una restricción de clase base de System.ValueType . El tipo System.ValueType no se puede usar
como restricción de clase base. En el ejemplo siguiente se muestran las restricciones class y struct :
En un contexto que admite un valor NULL en C# 8.0 y versiones posteriores, la restricción class requiere que
un tipo sea un tipo de referencia que no acepte valores NULL. Para permitir tipos de referencia que admitan un
valor NULL, use la restricción class? , que permite tipos de referencia que aceptan y que no aceptan valores
NULL.
La cláusula where puede incluir la restricción notnull . La restricción notnull limita el parámetro de tipo a
tipos que no aceptan valores NULL. El tipo puede ser un tipo de valor o un tipo de referencia que no acepta
valores NULL. La restricción notnull está disponible a partir C# 8.0 para el código compilado en un contexto
nullable enable . A diferencia de otras restricciones, si un argumento de tipo infringe la restricción notnull , el
compilador genera una advertencia en lugar de un error. Las advertencias solo se generan en un contexto
nullable enable .
La incorporación de tipos de referencia que aceptan valores NULL introduce una ambigüedad potencial en el
significado de T? en los métodos genéricos. Si T es un elemento struct , T? es igual que
System.Nullable<T>. Sin embargo, si T es un tipo de referencia, T? significa que null es un valor válido. La
ambigüedad surge porque invalidar métodos no puede incluir restricciones. La restricción default nueva
resuelve esta ambigüedad. Se agregará cuando una clase base o interfaz declare dos sobrecargas de un método;
una que especifica la restricción struct , y otra que no tiene aplicada la restricción struct ni la class :
La restricción default se usa para especificar que la clase derivada invalida el método sin la restricción en la
clase derivada o la implementación de interfaz explícita. Solo es válido en métodos que invalidan métodos base
o implementaciones de interfaz explícitas:
public class D : B
{
// Without the "default" constraint, the compiler tries to override the first method in B
public override void M<T>(T? item) where T : default { }
}
IMPORTANT
Las declaraciones genéricas que incluyen la restricción notnull se pueden usar en un contexto donde se desconoce que
se aceptan valores NULL, pero el compilador no aplica la restricción.
#nullable enable
class NotNullContainer<T>
where T : notnull
{
}
#nullable restore
La cláusula where también podría incluir una restricción unmanaged . La restricción unmanaged limita el
parámetro de tipo a los tipos conocidos como tipos no administrados. La restricción unmanaged hace que sea
más fácil escribir código de interoperabilidad de bajo nivel en C#. Esta restricción habilita las rutinas reutilizables
en todos los tipos no administrados. La restricción unmanaged no se puede combinar con las restricciones
class o struct . La restricción unmanaged exige que el tipo sea struct :
class UnManagedWrapper<T>
where T : unmanaged
{ }
La cláusula where también podría incluir una restricción de constructor, new() . Esta restricción hace posible
crear una instancia de un parámetro de tipo con el operador new . La restricción new() permite que el
compilador sepa que cualquier argumento de tipo especificado debe tener accesible un constructor sin
parámetros. Por ejemplo:
La restricción new() aparece en último lugar en la cláusula where . La restricción new() no se puede combinar
con las restricciones struct o unmanaged . Todos los tipos que cumplan esas restricciones deben tener un
constructor sin parámetros accesible, lo que hace que la restricción new() sea redundante.
Con varios parámetros de tipo, use una cláusula where para cada parámetro de tipo, por ejemplo:
namespace CodeExample
{
class Dictionary<TKey, TVal>
where TKey : IComparable<TKey>
where TVal : IMyInterface
{
public void Add(TKey key, TVal val) { }
}
}
También puede asociar restricciones a parámetros de tipo de métodos genéricos, como se muestra en el
ejemplo siguiente:
Observe que la sintaxis para describir las restricciones de parámetro de tipo en delegados es igual que la de
métodos:
Para obtener información sobre los delegados genéricos, vea Delegados genéricos.
Para obtener más información sobre la sintaxis y el uso de restricciones, vea Restricciones de tipos de
parámetros.
Consulte también
Referencia de C#
Guía de programación de C#
Introducción a los genéricos
new (restricción)
Restricciones de tipos de parámetros
base (Referencia de C#)
16/09/2021 • 2 minutes to read
La palabra clave base se usa para acceder a los miembros de la clase base desde una clase derivada:
Llamar a un método en la clase base que haya sido reemplazado por otro método.
Especificar a qué constructor de clase base se debe llamar cuando se crean instancias de la clase derivada.
Solo se permite el acceso a una clase base en un constructor, un método de instancia o un descriptor de acceso
de propiedad de instancia.
Usar la palabra clave base desde dentro de un método estático constituye un error.
La clase base a la que se obtiene acceso es la especificada en la declaración de clase. Por ejemplo, si especifica
class ClassB : ClassA , se obtiene acceso a los miembros de ClassA desde ClassB, independientemente de la
clase base de ClassA.
Ejemplo 1
En este ejemplo, la clase base, Person , y la clase derivada, Employee , tienen un método denominado Getinfo .
Mediante el uso de la palabra clave base , es posible llamar al método Getinfo en la clase base desde la clase
derivada.
public class Person
{
protected string ssn = "444-55-6666";
protected string name = "John L. Malgraine";
class TestClass
{
static void Main()
{
Employee E = new Employee();
E.GetInfo();
}
}
/*
Output
Name: John L. Malgraine
SSN: 444-55-6666
Employee ID: ABC567EFG
*/
Ejemplo 2
En este ejemplo se muestra cómo especificar el constructor de clase base al que se llama al crear instancias de
una clase derivada.
public class BaseClass
{
int num;
public BaseClass()
{
Console.WriteLine("in BaseClass()");
}
public BaseClass(int i)
{
num = i;
Console.WriteLine("in BaseClass(int i)");
}
Consulte también
Referencia de C#
Guía de programación de C#
Palabras clave de C#
this
this (Referencia de C#)
16/09/2021 • 2 minutes to read
La palabra clave this hace referencia a la instancia actual de la clase y también se usa como modificador del
primer parámetro de un método de extensión.
NOTE
En este artículo se describe el uso de this con instancias de clase. Para obtener más información sobre su uso en
métodos de extensión, vea Métodos de extensión.
CalcTax(this);
Las funciones miembro estáticas no tienen un puntero this , debido a que existen en el nivel de clase y no
como parte de un objeto. Es un error hacer referencia a this en un método estático.
Ejemplo
En este ejemplo, se usa this para calificar los miembros de la clase Employee , name y alias , que están
ocultos por nombres similares. También se usa para pasar un objeto al método CalcTax , que pertenece a otra
clase.
class Employee
{
private string name;
private string alias;
private decimal salary = 3000.00m;
// Constructor:
public Employee(string name, string alias)
{
// Use this to qualify the fields, name and alias:
this.name = name;
this.alias = alias;
}
// Printing method:
public void printEmployee()
{
Console.WriteLine("Name: {0}\nAlias: {1}", name, alias);
// Passing the object to the CalcTax method by using this:
Console.WriteLine("Taxes: {0:C}", Tax.CalcTax(this));
}
class Tax
{
public static decimal CalcTax(Employee E)
{
return 0.08m * E.Salary;
}
}
class MainClass
{
static void Main()
{
// Create objects:
Employee E1 = new Employee("Mingda Pan", "mpan");
// Display results:
E1.printEmployee();
}
}
/*
Output:
Name: Mingda Pan
Alias: mpan
Taxes: $240.00
*/
Vea también
Referencia de C#
Guía de programación de C#
Palabras clave de C#
base
Métodos
null (Referencia de C#)
16/09/2021 • 2 minutes to read
La palabra clave null es un literal que representa una referencia nula que no hace referencia a ningún objeto.
null es el valor predeterminado de las variables de tipo de referencia. Los tipos de valor normales no pueden
ser NULL, excepto los tipos de valor que admiten un valor NULL.
En el ejemplo siguiente se muestran algunos comportamientos de la palabra clave null :
class Program
{
class MyClass
{
public void MyMethod() { }
}
// Returns true.
Console.WriteLine("null == null is {0}", null == null);
La palabra clave de tipo bool es un alias para el tipo de estructura de .NET System.Boolean que representa un
valor booleano que puede ser true o false .
Para realizar operaciones lógicas con valores del tipo bool , use operadores lógicos booleanos. El tipo bool es
el tipo de resultado de los operadores de comparación e igualdad. Una expresión bool puede ser una expresión
condicional de control en las instrucciones if, do, while y for, así como en el operador condicional ?: .
El valor predeterminado del tipo bool es false .
Literales
Puede usar los literales true y false para inicializar una variable bool o para pasar un valor bool :
Conversiones
C# solo proporciona dos conversiones que implican al tipo bool . Son una conversión implícita al tipo bool?
que acepta valores NULL correspondiente y una conversión explícita del tipo bool? . Sin embargo, .NET
proporciona métodos adicionales que se pueden usar para realizar una conversión al tipo bool , o bien
revertirla. Para obtener más información, vea la sección Convertir a y desde valores booleanos de la página de
referencia de la API System.Boolean.
Vea también
Referencia de C#
Tipos de valor
operadores true y false
default (referencia de C#)
16/09/2021 • 2 minutes to read
Vea también
Referencia de C#
Palabras clave de C#
Palabras clave contextuales (referencia de C#)
16/09/2021 • 2 minutes to read
Las palabras clave contextuales se usan para proporcionar un significado específico en el código, pero no son
una palabra reservada en C#. En esta sección se presentan las siguientes palabras clave contextuales:
Todas las palabras clave de consulta introducidas en C# 3.0 también son contextuales. Para obtener más
información, vea Palabras clave para consultas (LINQ).
Vea también
Referencia de C#
Palabras clave de C#
Operadores y expresiones de C#
add (Referencia de C#)
16/09/2021 • 2 minutes to read
La palabra clave contextual add se usa para definir un descriptor de acceso de eventos personalizado que se
invoca cuando el código de cliente se suscribe a su evento. Si proporciona un descriptor de acceso add
personalizado, también debe proporcionar un descriptor de acceso remove.
Ejemplo
En el ejemplo siguiente se muestra un evento que tiene descriptores de acceso add y remove personalizados.
Para obtener el ejemplo completo, consulte Procedimiento Implementar eventos de interfaz.
Normalmente, no necesita proporcionar sus propios descriptores de acceso de eventos personalizados. Los
descriptores de acceso que se generan automáticamente mediante el compilador cuando declara un evento son
suficientes para la mayoría de escenarios.
Vea también
Eventos
get (Referencia de C#)
16/09/2021 • 2 minutes to read
La palabra clave get define un método de descriptor de acceso en una propiedad o un indizador que devuelve
el valor de la propiedad o el elemento del indizador. Para obtener más información, consulte Propiedades,
Propiedades autoimplementadas e Indexers Indizadores.
En el ejemplo siguiente se definen unos descriptores de acceso get y set para una propiedad denominada
Seconds . Usa un campo privado denominado _seconds para respaldar el valor de la propiedad.
class TimePeriod
{
private double _seconds;
A menudo, el descriptor de acceso get consta de una única instrucción que devuelve un valor, como en el
ejemplo anterior. A partir de C# 7.0, se puede implementar el descriptor de acceso get como un miembro con
forma de expresión. En el ejemplo siguiente se implementan los descriptores de acceso get y set como
miembros con forma de expresión.
class TimePeriod
{
private double _seconds;
En los casos sencillos en los que los descriptores de acceso get y set de una propiedad no realizan ninguna
operación aparte de establecer o recuperar un valor en un campo de respaldo privado, puede aprovechar la
compatibilidad del compilador de C# con las propiedades implementadas automáticamente. En el ejemplo
siguiente se implementa Hours como una propiedad implementada automáticamente.
class TimePeriod2
{
public double Hours { get; set; }
}
En C# 9 y versiones posteriores, la palabra clave init define un método de descriptor de acceso en una
propiedad o un indizador. Un establecedor de solo inicio asigna un valor a la propiedad o al elemento de
indizador únicamente durante la construcción del objeto. Para obtener más información y ejemplos, vea
Propiedades, Propiedades autoimplementadas e Indexadores.
En el ejemplo siguiente se definen los descriptores de acceso get y init para una propiedad denominada
Seconds . Usa un campo privado denominado _seconds para respaldar el valor de la propiedad.
class InitExample
{
private double _seconds;
A menudo, el descriptor de acceso init consta de una única instrucción que asigna un valor, como en el
ejemplo anterior. Puede implementar el descriptor de acceso init como un miembro con forma de expresión.
En el ejemplo siguiente se implementan los descriptores de acceso get y init como miembros con forma de
expresión.
class InitExampleExpressionBodied
{
private double _seconds;
En los casos sencillos en los que los descriptores de acceso get y init de una propiedad no realizan ninguna
operación aparte de establecer o recuperar un valor en un campo de respaldo privado, puede aprovechar la
compatibilidad del compilador de C# con las propiedades implementadas automáticamente. En el ejemplo
siguiente se implementa Hours como una propiedad implementada automáticamente.
class InitExampleAutoProperty
{
public double Hours { get; init; }
}
Las definiciones de tipo parcial permiten dividir la definición de una clase, estructura, interfaz o registro en
varios archivos.
En File1.cs:
namespace PC
{
partial class A
{
int num = 0;
void MethodA() { }
partial void MethodC();
}
}
La declaración en File2.cs:
namespace PC
{
partial class A
{
void MethodB() { }
partial void MethodC() { }
}
}
Observaciones
Dividir un tipo de clase, estructura o interfaz en varios archivos puede resultar útil cuando trabaja con proyectos
de gran tamaño o con código generado automáticamente, como el proporcionado por el Diseñador de
Windows Forms. Un tipo parcial puede contener un método parcial. Para más información, vea Clases y
métodos parciales.
Consulte también
Referencia de C#
Guía de programación de C#
Modificadores
Introducción a los genéricos
partial (Método) (Referencia de C#)
16/09/2021 • 2 minutes to read
Un método parcial tiene su signatura definida en una parte de un tipo parcial y su implementación definida en
otra parte del tipo. Los métodos parciales permiten a los diseñadores de clases proporcionar enlaces de método,
similares a los controladores de eventos, que los desarrolladores pueden decidir implementar o no. Si el
desarrollador no proporciona una implementación, el compilador quita la signatura en tiempo de compilación.
Se aplican las siguientes condiciones a los métodos parciales:
Las declaraciones deben comenzar con la palabra clave contextual partial.
Las signaturas de ambas partes del tipo parcial deben coincidir.
No es necesario que un método parcial tenga una implementación en los casos siguientes:
No tiene ningún modificador de accesibilidad (incluido el predeterminado private).
Devuelve void.
No tiene parámetros out.
No tiene ninguno de los modificadores virtual, override, sealed, new o extern.
Cualquier método que no cumpla todas estas restricciones (por ejemplo, public virtual partial void ) debe
proporcionar una implementación.
En el ejemplo siguiente se muestra un método parcial definido en dos partes de una clase parcial:
namespace PM
{
partial class A
{
partial void OnSomethingHappened(string s);
}
Los métodos parciales también pueden ser útiles en combinación con los generadores de código fuente. Por
ejemplo, se podría definir una expresión regular con el siguiente patrón:
[RegexGenerated("(dog|cat|fish)")]
partial bool IsPetMatch(string input);
La palabra clave contextual remove se usa para definir un descriptor de acceso de eventos personalizado que se
invoca cuando el código de cliente cancela la suscripción a su evento. Si proporciona un descriptor de acceso
remove personalizado, también debe proporcionar un descriptor de acceso add.
Ejemplo
En el ejemplo siguiente, se muestra un evento con descriptores de acceso add y remove personalizados. Para
obtener el ejemplo completo, consulte Procedimiento Implementar eventos de interfaz.
Normalmente, no necesita proporcionar sus propios descriptores de acceso de eventos personalizados. Los
descriptores de acceso que se generan automáticamente mediante el compilador cuando declara un evento son
suficientes para la mayoría de escenarios.
Vea también
Eventos
set (Referencia de C#)
16/09/2021 • 2 minutes to read
La palabra clave set define un método de descriptor de acceso en una propiedad o indexador que asigna el
valor de la propiedad o del elemento del indexador. Para obtener más información y ejemplos, vea Propiedades,
Propiedades autoimplementadas e Indexadores.
En el ejemplo siguiente se definen unos descriptores de acceso get y set para una propiedad denominada
Seconds . Usa un campo privado denominado _seconds para respaldar el valor de la propiedad.
class TimePeriod
{
private double _seconds;
A menudo, el descriptor de acceso set consta de una única instrucción que asigna un valor, como en el ejemplo
anterior. A partir de C# 7.0, se puede implementar el descriptor de acceso set como un miembro con forma de
expresión. En el ejemplo siguiente se implementan los descriptores de acceso get y set como miembros con
forma de expresión.
class TimePeriod
{
private double _seconds;
En los casos sencillos en los que los descriptores de acceso get y set de una propiedad no realizan ninguna
operación aparte de establecer o recuperar un valor en un campo de respaldo privado, puede aprovechar la
compatibilidad del compilador de C# con las propiedades implementadas automáticamente. En el ejemplo
siguiente se implementa Hours como una propiedad implementada automáticamente.
class TimePeriod2
{
public double Hours { get; set; }
}
La palabra clave contextual when se usa para especificar una condición de filtro en los siguientes contextos:
En la instrucción catch de un bloque try/catch o try/catch/finally.
Como restricción de caso en la instrucción switch .
Como restricción de caso en la expresión switch .
donde expr es una expresión que se evalúa como un valor booleano. Si devuelve true , el controlador de
excepciones se ejecuta; si devuelve false , no se ejecuta.
En el ejemplo siguiente se usa la palabra clave when para ejecutar condicionalmente controladores para una
HttpRequestException según el texto del mensaje de excepción.
using System;
using System.Net.Http;
using System.Threading.Tasks;
class Program
{
static void Main()
{
Console.WriteLine(MakeRequest().Result);
}
Vea también
try/catch (Instrucción)
try/catch/finally (Instrucción)
value (Referencia de C#)
16/09/2021 • 2 minutes to read
La palabra clave contextual value se usa en el descriptor de acceso set de las declaraciones propiedad y
indizador. Es parecido a un parámetro de entrada de un método. El término value hace referencia al valor que
el código de cliente intenta asignar a la propiedad o indizador. En el ejemplo siguiente, MyDerivedClass tiene una
propiedad denominada Name que usa el parámetro value para asignar una nueva cadena al campo de
respaldo name . Desde el punto de vista del código de cliente, la operación se escribe como una simple
asignación.
class MyBaseClass
{
// virtual auto-implemented property. Overrides can only
// provide specialized behavior if they implement get and set accessors.
public virtual string Name { get; set; }
Cuando se usa la yield palabra clave contextual en una instrucción, se indica que el método, el operador o el
descriptor de acceso get en el que aparece es un iterador. Al usar yield para definir un iterador ya no es
necesaria una clase adicional explícita (la clase que retiene el estado para una enumeración, consulte
IEnumerator<T> para ver un ejemplo) al implementar los patrones IEnumerable y IEnumerator para un tipo de
colección personalizado.
En el ejemplo siguiente se muestran las dos formas de la instrucción yield .
Comentarios
Utilice una instrucción yield return para devolver cada elemento de uno en uno.
La secuencia devuelta desde un método iterador se puede consumir mediante la instrucción foreach o una
consulta LINQ. Cada iteración del bucle foreach llama al método iterador. Cuando se alcanza una instrucción
yield return en el método iterador, se devuelve expression y se conserva la ubicación actual en el código. La
ejecución se reinicia desde esa ubicación la próxima vez que se llama a la función del iterador.
Cuando el iterador devuelve una interfaz System.Collections.Generic.IAsyncEnumerable<T>, esa secuencia se
puede consumir de forma asincrónica mediante una instrucción await foreach. La iteración del bucle es análoga
a la instrucción foreach . La diferencia es que cada iteración se puede suspender para una operación asincrónica
antes de devolver la expresión para el elemento siguiente.
Puede usar una instrucción yield break para finalizar la iteración.
Para obtener más información sobre los iteradores, vea Iteradores.
Control de excepciones
Una instrucción yield return no puede encontrarse en un bloque try-catch. Una instrucción yield return
puede encontrarse en el bloque try de una instrucción try-finally.
Una instrucción yield break puede encontrarse en un bloque try o un bloque catch, pero no en un bloque
finally.
Si el cuerpo foreach o await foreach (fuera del método iterador) produce una excepción, se ejecuta un bloque
finally en el método iterador.
Implementación técnica
El código siguiente devuelve un valor IEnumerable<string> desde un método iterador y, a continuación, recorre
sus elementos en iteración.
La llamada a MyIteratorMethod no ejecuta el cuerpo del método. En su lugar, la llamada devuelve un valor
IEnumerable<string> en la variable elements .
En una iteración del bucle foreach , se llama al método MoveNext para elements . Esta llamada ejecuta el
cuerpo de MyIteratorMethod hasta que se alcanza la siguiente instrucción yield return . La expresión devuelta
por la instrucción yield return determina no solo el valor de la variable element para que la utilice el cuerpo
del bucle, sino también la propiedad Current de elements , que es un valor IEnumerable<string> .
En cada iteración subsiguiente del bucle foreach , la ejecución del cuerpo del iterador continúa desde donde se
dejó, deteniéndose de nuevo al alcanzar una instrucción yield return . El bucle foreach se completa al alcanzar
el fin del método iterador o una instrucción yield break .
El código siguiente devuelve un valor IAsyncEnumerable<string> desde un método iterador y, a continuación,
recorre sus elementos en iteración.
En una iteración del bucle await foreach , se llama al método IAsyncEnumerator<T>.MoveNextAsync para
elements . El valor devuelto de System.Threading.Tasks.ValueTask<TResult> por MoveNext se completa cuando
se alcanza el siguiente valor yield return .
En cada iteración subsiguiente del bucle await foreach , la ejecución del cuerpo del iterador continúa desde
donde se dejó, deteniéndose de nuevo al alcanzar una instrucción yield return . El bucle await foreach se
completa al alcanzar el fin del método iterador o una instrucción yield break .
Ejemplos
El ejemplo siguiente tiene una instrucción yield return que está dentro de un bucle for . Cada iteración del
cuerpo de instrucción foreach en el método Main crea una llamada a la función de iterador Power . Cada
llamada a la función de iterador prosigue con la siguiente ejecución de la instrucción yield return , que se
produce durante la siguiente iteración del bucle for .
El tipo de valor devuelto del método iterador es IEnumerable, que es un tipo de interfaz de iteradores. Cuando
se llama al método iterador, este devuelve un objeto enumerable que contiene las potencias de un número.
En el ejemplo siguiente se muestra un descriptor de acceso get que es un iterador. En el ejemplo, cada una de
las instrucciones yield return devuelve una instancia de una clase definida por el usuario.
public static class GalaxyClass
{
public static void ShowGalaxies()
{
var theGalaxies = new Galaxies();
foreach (Galaxy theGalaxy in theGalaxies.NextGalaxy)
{
Debug.WriteLine(theGalaxy.Name + " " + theGalaxy.MegaLightYears.ToString());
}
}
Vea también
Referencia de C#
Guía de programación de C#
foreach, in
Iteradores
Palabras clave de consulta (Referencia de C#)
16/09/2021 • 2 minutes to read
En esta sección, se incluyen las palabras clave contextuales que se usan en expresiones de consulta.
En esta sección
C L Á USUL A DESC RIP C IÓ N
Vea también
Palabras clave de C#
LINQ (Language Integrated Query)
LINQ en C#
from (Cláusula, Referencia de C#)
16/09/2021 • 6 minutes to read
Una expresión de consulta debe comenzar con una cláusula from , Además, una expresión de consulta puede
contener subconsultas, que también comienzan con una cláusula from . La cláusula from especifica lo
siguiente:
El origen de datos en el que se ejecutará la consulta o subconsulta.
Una variable de rango local que representa cada elemento de la secuencia de origen.
Tanto la variable de rango como el origen de datos están fuertemente tipados. El origen de datos al que se hace
referencia en la cláusula from debe tener un tipo de IEnumerable, IEnumerable<T> o un tipo derivado como
IQueryable<T>.
En el ejemplo siguiente, numbers es el origen de datos y num es la variable de rango. Tenga en cuenta que
ambas variables están fuertemente tipadas a pesar de que se usa la palabra clave var.
class LowNums
{
static void Main()
{
// A simple data source.
int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
Variable de rango
El compilador deduce el tipo de la variable de rango cuando el origen de datos implementa IEnumerable<T>.
Por ejemplo, si el origen tiene un tipo de IEnumerable<Customer> , entonces se deduce que la variable de rango es
Customer . La única vez en que debe especificar el tipo explícitamente es cuando el origen es un tipo
IEnumerable no genérico como ArrayList. Para obtener más información, vea Procedimiento para consultar un
objeto ArrayList con LINQ (C#).
En el ejemplo anterior, num se deduce que es de tipo int . Como la variable de rango está fuertemente tipada,
puede llamar a los métodos en ella o usarla en otras operaciones. Por ejemplo, en lugar de escribir select num ,
podría escribir select num.ToString() para hacer que la expresión de consulta devuelva una secuencia de
cadenas en lugar de enteros. O podría escribir select num + 10 para hacer que la expresión devuelva la
secuencia 14, 11, 13, 12, 10. Para obtener más información, vea Cláusula select.
La variable de rango es como una variable de iteración en una instrucción foreach excepto por una diferencia
muy importante: una variable de rango realmente nunca almacena datos del origen. Es solo una comodidad
sintáctica que permite a la consulta describir lo que ocurrirá cuando se ejecute la consulta. Para obtener más
información, vea Introducción a las consultas LINQ (C#).
// Use a compound from to access the inner sequence within each element.
// Note the similarity to a nested foreach statement.
var scoreQuery = from student in students
from score in student.Scores
where score > 90
select new { Last = student.LastName, score };
class CompoundFrom2
{
static void Main()
{
char[] upperCase = { 'A', 'B', 'C' };
char[] lowerCase = { 'x', 'y', 'z' };
Console.WriteLine("Filtered non-equijoin:");
// Rest the mouse pointer over joinQuery2 to verify its type.
foreach (var pair in joinQuery2)
{
Console.WriteLine("{0} is matched to {1}", pair.lower, pair.upper);
}
Vea también
Palabras clave para consultas (LINQ)
Language-Integrated Query (LINQ)
where (Cláusula, Referencia de C#)
16/09/2021 • 3 minutes to read
La cláusula where se usa en una expresión de consulta para especificar los elementos del origen de datos que
se devuelven en dicha expresión. Aplica una condición booleana (predicate) a cada elemento de origen (al que
hace referencia la variable de rango) y devuelve aquellos en los que la condición especificada se cumple. Puede
que una sola expresión de consulta contenga varias cláusulas where y que una sola cláusula contenga varias
subexpresiones de predicado.
Ejemplo 1
En el ejemplo siguiente, la cláusula where filtra todos los números excepto los que son inferiores a cinco. Si la
cláusula where se quita, se devolverán todos los números del origen de datos. La expresión num < 5 es el
predicado que se aplica a cada elemento.
class WhereSample
{
static void Main()
{
// Simple data source. Arrays support IEnumerable<T>.
int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
Ejemplo 2
En una sola cláusula where , se pueden especificar todos los predicados que sean necesarios mediante los
operadores && y ||. En el ejemplo siguiente, la consulta especifica dos predicados para seleccionar únicamente
los números pares que sean inferiores a cinco.
class WhereSample2
{
static void Main()
{
// Data source.
int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
Ejemplo 3
Puede que una cláusula where contenga uno o más métodos que devuelvan valores booleanos. En el ejemplo
siguiente, la cláusula where usa un método para determinar si el valor actual de la variable de rango es par o
impar.
class WhereSample3
{
static void Main()
{
// Data source
int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
Observaciones
La cláusula where es un mecanismo de filtrado. Se puede colocar prácticamente en cualquier parte en una
expresión de consulta, pero no puede ser la primera ni la última cláusula. Puede que una cláusula where
aparezca antes o después de una cláusula group, en función de que haya que filtrar los elementos de origen
antes o después de agruparlos.
Si un predicado especificado no es válido para los elementos del origen de datos, se producirá un error en
tiempo de compilación. Esta es una de las ventajas de la comprobación fuertemente tipada que ofrece LINQ.
En tiempo de compilación, la palabra clave where se convierte en una llamada al método de operador de
consulta estándar Where.
Vea también
Palabras clave para consultas (LINQ)
from (cláusula)
select (cláusula)
Filtrado de datos
LINQ en C#
Language-Integrated Query (LINQ)
select (Cláusula, Referencia de C#)
16/09/2021 • 6 minutes to read
En una expresión de consulta, la cláusula select especifica el tipo de valores que se producirán cuando se
ejecute la consulta. El resultado se basa en la evaluación de todas las cláusulas anteriores y en cualquier
expresión de la propia cláusula select . Una expresión de consulta debe finalizar con una cláusula select o
con una cláusula group.
En el ejemplo siguiente, se muestra una cláusula select simple en una expresión de consulta.
class SelectSample1
{
static void Main()
{
//Create the data source
List<int> Scores = new List<int>() { 97, 92, 81, 60 };
El tipo de la secuencia generada por la cláusula select determina el tipo de la variable de consulta
queryHighScores . En el caso más simple, la cláusula select simplemente especifica la variable de rango. Esto
hace que la secuencia devuelta contenga elementos del mismo tipo que el origen de datos. Para obtener más
información, vea Relaciones entre tipos en operaciones de consulta LINQ. En cambio, la cláusula select
también proporciona un mecanismo eficaz para transformar (o proyectar) el origen de datos en nuevos tipos.
Para obtener más información, vea Transformaciones de datos con LINQ (C#).
Ejemplo
En el ejemplo siguiente, se muestran las distintas formas que puede tener una cláusula select . En cada
consulta, tenga en cuenta la relación entre la cláusula select y el tipo de la variable de consulta ( studentQuery1
, studentQuery2 , etc.).
class SelectSample2
{
// Define some classes
public class Student
{
public string First { get; set; }
public string Last { get; set; }
public int ID { get; set; }
public List<int> Scores;
public ContactInfo GetContactInfo(SelectSample2 app, int id)
{
{
ContactInfo cInfo =
(from ci in app.contactList
where ci.ID == id
select ci)
.FirstOrDefault();
return cInfo;
}
Como se muestra en studentQuery8 en el ejemplo anterior, a veces es posible que quiera que los elementos de
la secuencia devuelta contengan solo un subconjunto de las propiedades de los elementos de origen. Al
mantener la secuencia devuelta lo más pequeña posible, puede reducir los requisitos de memoria y aumentar la
velocidad de ejecución de la consulta. Puede hacerlo al crear un tipo anónimo en la cláusula select y usar un
inicializador de objeto para inicializarlo con las propiedades adecuadas del elemento de origen. Para obtener un
ejemplo de cómo hacerlo, consulte Inicializadores de objeto y de colección.
Observaciones
En tiempo de compilación, la cláusula select se convierte en una llamada de método al operador de consulta
estándar Select.
Vea también
Referencia de C#
Palabras clave para consultas (LINQ)
from (cláusula)
partial (Método) (Referencia de C#)
Tipos anónimos
LINQ en C#
Language-Integrated Query (LINQ)
group (Cláusula, Referencia de C#)
16/09/2021 • 9 minutes to read
La cláusula group devuelve una secuencia de objetos IGrouping<TKey,TElement> que contienen cero o más
elementos que coinciden con el valor de clave del grupo. Por ejemplo, puede agrupar una secuencia de cadenas
según la primera letra de cada cadena. En este caso, la primera letra es la clave, es de tipo char y se almacena en
la propiedad Key de cada objeto IGrouping<TKey,TElement>. El compilador deduce el tipo de la clave.
Puede finalizar una expresión de consulta con una cláusula group , como se muestra en el ejemplo siguiente:
Si quiere realizar operaciones de consulta adicionales en cada grupo, puede especificar un identificador
temporal mediante la palabra clave contextual into. Cuando se usa into , es necesario continuar con la consulta
y finalmente terminarla con una instrucción select u otra cláusula group , como se muestra en el extracto
siguiente:
En la sección Ejemplo de este artículo se proporcionan ejemplos más completos sobre el uso de group con y
sin into .
// Same as previous example except we use the entire last name as a key.
// Query variable is an IEnumerable<IGrouping<string, Student>>
var studentQuery3 =
from student in students
group student by student.Last;
return students;
}
class GroupSample2
{
// The element type of the data source.
public class Student
{
public string First { get; set; }
public string Last { get; set; }
public int ID { get; set; }
public List<int> Scores;
}
return students;
}
Use un tipo con nombre si debe pasar la variable de consulta a otro método. Cree una clase especial usando las
propiedades autoimplementadas para las claves y, luego, invalide los métodos Equals y GetHashCode. También
puede usar un struct, en cuyo caso no es estrictamente necesario invalidar esos métodos. Para más información,
consulte Procedimiento Implementar una clase ligera con propiedades autoimplementadas y Procedimiento
para buscar archivos duplicados en un árbol de directorios. El último artículo contiene un ejemplo de código en
el que se muestra cómo usar una clave compuesta con un tipo con nombre.
Ejemplo 1
En el ejemplo siguiente se muestra el patrón estándar para ordenar los datos de origen en grupos cuando no se
aplica ninguna lógica de consulta adicional a los grupos. Esto se denomina agrupación sin continuación. Los
elementos de una matriz de cadenas se agrupan por la primera letra. El resultado de la consulta es un tipo
IGrouping<TKey,TElement> que contiene una propiedad Key pública de tipo char y una colección
IEnumerable<T> que contiene cada elemento de la agrupación.
El resultado de una cláusula group es una secuencia de secuencias. Por consiguiente, para tener acceso a los
elementos individuales de cada grupo devuelto, use un bucle foreach anidado dentro del bucle que recorre en
iteración las claves de grupo, como se muestra en el ejemplo siguiente.
class GroupExample1
{
static void Main()
{
// Create a data source.
string[] words = { "blueberry", "chimpanzee", "abacus", "banana", "apple", "cheese" };
Ejemplo 2
En este ejemplo se muestra cómo aplicar lógica adicional a los grupos después de haberlos creado, mediante el
uso de una continuación con into . Para obtener más información, vea into. En el ejemplo siguiente se consulta
cada grupo para seleccionar solo aquellos cuyo valor de clave sea una vocal.
class GroupClauseExample2
{
static void Main()
{
// Create the data source.
string[] words2 = { "blueberry", "chimpanzee", "abacus", "banana", "apple", "cheese", "elephant",
"umbrella", "anteater" };
Observaciones
En tiempo de compilación, las cláusulas group se convierten en llamadas al método GroupBy.
Vea también
IGrouping<TKey,TElement>
GroupBy
ThenBy
ThenByDescending
Palabras clave para consultas
Language-Integrated Query (LINQ)
Crear grupos anidados
Agrupar los resultados de consultas
Realizar una subconsulta en una operación de agrupación
into (Referencia de C#)
16/09/2021 • 2 minutes to read
La palabra clave contextual into puede usarse para crear un identificador temporal para almacenar los
resultados de una cláusula group, join o select en un nuevo identificador. Este identificador puede ser un
generador de comandos de consulta adicionales. Cuando se usa en una cláusula group o select , el uso del
nuevo identificador se denomina a veces una continuación.
Ejemplo
En el ejemplo siguiente, se muestra el uso de la palabra clave into para habilitar un identificador temporal
fruitGroup que tiene un tipo deducido de IGrouping . Mediante el identificador, puede invocar el método Count
en cada grupo y seleccionar solo los grupos que contienen dos o más palabras.
class IntoSample1
{
static void Main()
{
// Execute the query. Note that we only iterate over the groups,
// not the items in each group
foreach (var item in wordGroups1)
{
Console.WriteLine(" {0} has {1} elements.", item.FirstLetter, item.Words);
}
El uso de into en una cláusula group solo es necesario cuando quiere realizar operaciones de consulta
adicionales en cada grupo. Para obtener más información, vea group (Cláusula).
Para obtener un ejemplo del uso de into en una cláusula join , vea join (Cláusula).
Vea también
Palabras clave para consultas (LINQ)
LINQ en C#
group (cláusula)
orderby (Cláusula, Referencia de C#)
16/09/2021 • 2 minutes to read
En una expresión de consulta, la cláusula orderby hace que la secuencia o subsecuencia (grupo) devuelta se
ordene de forma ascendente o descendente. Se pueden especificar varias claves para llevar a cabo una o varias
operaciones de ordenación secundaria. La ordenación se realiza mediante el comparador predeterminado del
tipo de elemento. El criterio de ordenación predeterminado es el ascendente. También puede especificar un
comparador personalizado. En cambio, solo está disponible mediante la sintaxis basada en métodos. Para
obtener más información, consulte Sorting Data (Ordenación de datos).
Ejemplo 1
En el ejemplo siguiente, la primera consulta ordena las palabras en orden alfabético a partir de la A y la segunda
consulta ordena las mismas palabras en orden descendente. (La palabra clave ascending es el valor de
ordenación predeterminado y puede omitirse).
class OrderbySample1
{
static void Main()
{
// Create a delicious data source.
string[] fruits = { "cherry", "apple", "blueberry" };
Descending:
cherry
blueberry
apple
*/
Ejemplo 2
En el ejemplo siguiente, se realiza una ordenación primaria por apellidos de los alumnos y, después, una
ordenación secundaria por sus nombres.
class OrderbySample2
{
// The element type of the data source.
public class Student
{
public string First { get; set; }
public string Last { get; set; }
public int ID { get; set; }
}
public static List<Student> GetStudents()
{
// Use a collection initializer to create the data source. Note that each element
// in the list contains an inner sequence of scores.
List<Student> students = new List<Student>
{
new Student {First="Svetlana", Last="Omelchenko", ID=111},
new Student {First="Claire", Last="O'Donnell", ID=112},
new Student {First="Sven", Last="Mortensen", ID=113},
new Student {First="Cesar", Last="Garcia", ID=114},
new Student {First="Debra", Last="Garcia", ID=115}
};
return students;
}
static void Main(string[] args)
{
// Create the data source.
List<Student> students = GetStudents();
// Now create groups and sort the groups. The query first sorts the names
// of all students so that they will be in alphabetical order after they are
// grouped. The second orderby sorts the group keys in alpha order.
var sortedGroups =
from student in students
orderby student.Last, student.First
group student by student.Last[0] into newGroup
orderby newGroup.Key
select newGroup;
sortedGroups:
G
Garcia, Cesar
Garcia, Debra
Garcia, Debra
M
Mortensen, Sven
O
O'Donnell, Claire
Omelchenko, Svetlana
*/
Observaciones
En tiempo de compilación, la cláusula orderby se convierte en una llamada al método OrderBy. Varias claves en
la cláusula orderby se convierten en llamadas al método ThenBy.
Vea también
Referencia de C#
Palabras clave para consultas (LINQ)
LINQ en C#
group (cláusula)
Language-Integrated Query (LINQ)
join (Cláusula, Referencia de C#)
16/09/2021 • 10 minutes to read
La cláusula join es útil para asociar elementos de secuencias de origen diferentes que no tienen ninguna
relación directa en el modelo de objetos. El único requisito es que los elementos de cada origen compartan
algún valor del que se pueda comparar la igualdad. Por ejemplo, imagínese que un distribuidor de comida tiene
una lista de proveedores de un determinado producto y una lista de compradores. Se puede usar una cláusula
join , por ejemplo, para crear una lista de los proveedores y compradores de dicho producto que se encuentran
en la misma región especificada.
La cláusula join toma dos secuencias de origen como entrada. Los elementos de cada secuencia deben ser o
deben contener una propiedad que se pueda comparar con una propiedad correspondiente en la otra secuencia.
La cláusula join compara la igualdad de las claves especificadas mediante la palabra clave especial equals .
Todas las combinaciones efectuadas por la cláusula join son combinaciones de igualdad. La forma de la salida
de una cláusula join depende del tipo específico de combinación que se va a efectuar. Estos son los tres tipos
de combinación más comunes:
Combinación interna
Combinación agrupada
Combinación externa izquierda
Combinación interna
En el ejemplo siguiente se muestra una combinación de igualdad interna simple. Esta consulta genera una
secuencia plana de pares de "nombre de producto y categoría". La misma cadena de categoría aparecerá en
varios elementos. Si un elemento de categories no tiene ningún products que coincida, dicha categoría no
aparecerá en los resultados.
var innerJoinQuery =
from category in categories
join prod in products on category.ID equals prod.CategoryID
select new { ProductName = prod.Name, Category = category.Name }; //produces flat sequence
Combinación agrupada
Una cláusula join con una expresión into se denomina "combinación agrupada".
var innerGroupJoinQuery =
from category in categories
join prod in products on category.ID equals prod.CategoryID into prodGroup
select new { CategoryName = category.Name, Products = prodGroup };
Las combinaciones agrupadas generan una secuencia de resultados jerárquicos que asocia los elementos de la
secuencia de origen izquierda con uno o más elementos coincidentes de la secuencia de origen derecha. Las
combinaciones agrupadas no tienen ningún equivalente en términos relacionales; son básicamente una
secuencia de matrices de objetos.
Si no se encuentra ningún elemento de la secuencia de origen derecha para que coincida con un elemento del
origen izquierdo, la cláusula join generará una matriz vacía para ese elemento. Por lo tanto, la combinación
agrupada es básicamente una combinación de igualdad interna, salvo que la secuencia del resultado se organiza
en grupos.
Si solo selecciona los resultados de una combinación agrupada, puede tener acceso a los elementos, pero no
podrá identificar la clave en la que coinciden. Por lo tanto, suele resultar más útil seleccionar los resultados de la
combinación agrupada en un nuevo tipo que también tenga el nombre de la clave, como se muestra en el
ejemplo anterior.
Por supuesto, también puede usar el resultado de una combinación agrupada como generador de otra
subconsulta:
var innerGroupJoinQuery2 =
from category in categories
join prod in products on category.ID equals prod.CategoryID into prodGroup
from prod2 in prodGroup
where prod2.UnitPrice > 2.50M
select prod2;
var leftOuterJoinQuery =
from category in categories
join prod in products on category.ID equals prod.CategoryID into prodGroup
from item in prodGroup.DefaultIfEmpty(new Product { Name = String.Empty, CategoryID = 0 })
select new { CatName = category.Name, ProdName = item.Name };
Para obtener más información, vea Realizar operaciones de combinación externa izquierda.
El operador de igualdad
La cláusula join efectúa una combinación de igualdad. En otras palabras, solo puede basar las coincidencias en
la igualdad de dos claves. No se admiten otros tipos de comparaciones, como "mayor que" o "no es igual a".
Para aclarar que todas las combinaciones son combinaciones de igualdad, la cláusula join usa la palabra clave
equals en lugar del operador == . La palabra clave equals solo se puede usar en una cláusula join y difiere
del operador == en un aspecto importante. Con equals , la clave izquierda usa la secuencia de origen externa,
mientras que la clave derecha usa el origen interno. El origen externo solo está en el ámbito del lado izquierdo
de equals , mientras que la secuencia de origen interna solo está en el ámbito del lado derecho.
Combinaciones de desigualdad
Puede efectuar combinaciones de desigualdad, combinaciones cruzadas y otras operaciones de combinación
personalizadas usando varias cláusulas from para introducir nuevas secuencias en una consulta de manera
independiente. Para obtener más información, vea Realizar operaciones de combinación personalizadas.
Claves compuestas
Puede probar la igualdad de varios valores mediante una clave compuesta. Para obtener más información, vea
Realizar una unión usando claves compuestas. Las claves compuestas también se pueden usar en una cláusula
group .
Ejemplo
En el ejemplo siguiente se comparan los resultados de una combinación interna, una combinación agrupada y
una combinación externa izquierda en los mismos orígenes de datos mediante las mismas claves coincidentes.
Se ha agregado algún código adicional a estos ejemplos para aclarar los resultados en la pantalla de la consola.
class JoinDemonstration
{
#region Data
class Product
{
public string Name { get; set; }
public int CategoryID { get; set; }
}
class Category
{
public string Name { get; set; }
public int ID { get; set; }
}
app.InnerJoin();
app.GroupJoin();
app.GroupInnerJoin();
app.GroupJoin3();
app.LeftOuterJoin();
app.LeftOuterJoin2();
void InnerJoin()
{
// Create the query that selects
// a property from each element.
var innerJoinQuery =
from category in categories
join prod in products on category.ID equals prod.CategoryID
select new { Category = category.ID, Product = prod.Name };
Console.WriteLine("InnerJoin:");
// Execute the query. Access results
// with a simple foreach statement.
foreach (var item in innerJoinQuery)
{
Console.WriteLine("{0,-10}{1}", item.Product, item.Category);
}
Console.WriteLine("InnerJoin: {0} items in 1 group.", innerJoinQuery.Count());
Console.WriteLine(System.Environment.NewLine);
}
void GroupJoin()
{
// This is a demonstration query to show the output
// of a "raw" group join. A more typical group join
// is shown in the GroupInnerJoin method.
var groupJoinQuery =
from category in categories
join prod in products on category.ID equals prod.CategoryID into prodGroup
select prodGroup;
Console.WriteLine("Simple GroupJoin:");
void GroupInnerJoin()
{
var groupJoinQuery2 =
from category in categories
orderby category.ID
join prod in products on category.ID equals prod.CategoryID into prodGroup
select new
{
Category = category.Name,
Products = from prod2 in prodGroup
orderby prod2.Name
select prod2
};
//Console.WriteLine("GroupInnerJoin:");
int totalItems = 0;
Console.WriteLine("GroupInnerJoin:");
foreach (var productGroup in groupJoinQuery2)
{
Console.WriteLine(productGroup.Category);
foreach (var prodItem in productGroup.Products)
{
totalItems++;
Console.WriteLine(" {0,-10} {1}", prodItem.Name, prodItem.CategoryID);
}
}
Console.WriteLine("GroupInnerJoin: {0} items in {1} named groups", totalItems,
groupJoinQuery2.Count());
Console.WriteLine(System.Environment.NewLine);
}
void GroupJoin3()
{
var groupJoinQuery3 =
from category in categories
join product in products on category.ID equals product.CategoryID into prodGroup
from prod in prodGroup
orderby prod.CategoryID
select new { Category = prod.CategoryID, ProductName = prod.Name };
//Console.WriteLine("GroupInnerJoin:");
int totalItems = 0;
Console.WriteLine("GroupJoin3:");
foreach (var item in groupJoinQuery3)
{
totalItems++;
Console.WriteLine(" {0}:{1}", item.ProductName, item.Category);
}
void LeftOuterJoin()
{
// Create the query.
var leftOuterQuery =
from category in categories
join prod in products on category.ID equals prod.CategoryID into prodGroup
select prodGroup.DefaultIfEmpty(new Product() { Name = "Nothing!", CategoryID = category.ID });
void LeftOuterJoin2()
{
// Create the query.
var leftOuterQuery2 =
from category in categories
join prod in products on category.ID equals prod.CategoryID into prodGroup
from item in prodGroup.DefaultIfEmpty()
select new { Name = item == null ? "Nothing!" : item.Name, CategoryID = category.ID };
InnerJoin:
Cola 1
Tea 1
Mustard 2
Pickles 2
Carrots 3
Bok Choy 3
Peaches 5
Melons 5
InnerJoin: 8 items in 1 group.
Unshaped GroupJoin:
Group:
Cola 1
Tea 1
Group:
Mustard 2
Pickles 2
Group:
Carrots 3
Bok Choy 3
Group:
Group:
Peaches 5
Peaches 5
Melons 5
Unshaped GroupJoin: 8 items in 5 unnamed groups
GroupInnerJoin:
Beverages
Cola 1
Tea 1
Condiments
Mustard 2
Pickles 2
Vegetables
Bok Choy 3
Carrots 3
Grains
Fruit
Melons 5
Peaches 5
GroupInnerJoin: 8 items in 5 named groups
GroupJoin3:
Cola:1
Tea:1
Mustard:2
Pickles:2
Carrots:3
Bok Choy:3
Peaches:5
Melons:5
GroupJoin3: 8 items in 1 group
Vea también
Palabras clave para consultas (LINQ)
Language-Integrated Query (LINQ)
Operaciones de combinación
group (cláusula)
Realizar operaciones de combinación externa izquierda
Realizar combinaciones internas
Realizar combinaciones agrupadas
Ordenar los resultados de una cláusula join
Realizar una unión usando claves compuestas
Sistemas de base de datos compatible para Visual Studio
let (Cláusula, Referencia de C#)
16/09/2021 • 2 minutes to read
En una expresión de consulta, a veces resulta útil almacenar el resultado de una subexpresión para usarlo en las
cláusulas siguientes. Puede hacer esto con la palabra clave let , que crea una variable de rango y la inicializa
con el resultado de la expresión que proporcione. Una vez inicializada con un valor, la variable de rango no se
puede usar para almacenar otro valor. En cambio, si la variable de rango contiene un tipo consultable, se puede
consultar.
Ejemplo
En el siguiente ejemplo, se usa let de dos maneras:
1. Para crear un tipo enumerable que se puede consultar.
2. Para habilitar la consulta para que llame a ToLower solo una vez en la variable de rango word . Sin usar
let , tendría que llamar a ToLower en cada predicado de la cláusula where .
class LetSample1
{
static void Main()
{
string[] strings =
{
"A penny saved is a penny earned.",
"The early bird catches the worm.",
"The pen is mightier than the sword."
};
Vea también
Referencia de C#
Palabras clave para consultas (LINQ)
LINQ en C#
Language-Integrated Query (LINQ)
Controlar excepciones en expresiones de consulta
ascending (Referencia de C#)
16/09/2021 • 2 minutes to read
La palabra clave contextual ascending se usa en la cláusula orderby en expresiones de consulta para especificar
que el criterio de ordenación es de menor a mayor. Como ascending es el criterio de ordenación
predeterminado, no tiene que especificarlo.
Ejemplo
En el ejemplo siguiente se muestra el uso de ascending en una cláusula orderby.
IEnumerable<string> sortAscendingQuery =
from vegetable in vegetables
orderby vegetable ascending
select vegetable;
Vea también
Referencia de C#
LINQ en C#
descending
descending (Referencia de C#)
16/09/2021 • 2 minutes to read
La palabra clave contextual descending se usa en la cláusula orderby en expresiones de consulta para
especificar que el criterio de ordenación es de mayor a menor.
Ejemplo
En el ejemplo siguiente se muestra el uso de descending en una cláusula orderby.
IEnumerable<string> sortDescendingQuery =
from vegetable in vegetables
orderby vegetable descending
select vegetable;
Vea también
Referencia de C#
LINQ en C#
ascending
on (Referencia de C#)
16/09/2021 • 2 minutes to read
La palabra clave contextual on se usa en la cláusula join de una expresión de consulta para especificar la
condición de combinación.
Ejemplo
En el ejemplo siguiente se muestra el uso de on en una cláusula join .
var innerJoinQuery =
from category in categories
join prod in products on category.ID equals prod.CategoryID
select new { ProductName = prod.Name, Category = category.Name };
Vea también
Referencia de C#
Language-Integrated Query (LINQ)
equals (Referencia de C#)
16/09/2021 • 2 minutes to read
La palabra clave contextual equals se usa en una cláusula join en una expresión de consulta para comparar
los elementos de dos secuencias. Para obtener más información, vea join (Cláusula, Referencia de C#).
Ejemplo
En el ejemplo siguiente se muestra el uso de la palabra clave equals en una cláusula join .
var innerJoinQuery =
from category in categories
join prod in products on category.ID equals prod.CategoryID
select new { ProductName = prod.Name, Category = category.Name };
Vea también
Language-Integrated Query (LINQ)
by (Referencia de C#)
16/09/2021 • 2 minutes to read
La palabra clave contextual by se usa en la cláusula group en una expresión de consulta para especificar cómo
deben agruparse los elementos devueltos. Para obtener más información, vea group (Cláusula).
Ejemplo
En el ejemplo siguiente se muestra el uso de la palabra clave contextual by en una cláusula group para
especificar que los estudiantes deben agruparse según la primera letra del apellido de cada estudiante.
Vea también
LINQ en C#
in (Referencia de C#)
16/09/2021 • 2 minutes to read
Vea también
Palabras clave de C#
Referencia de C#
Operadores y expresiones de C# (referencia de C#)
16/09/2021 • 5 minutes to read
C# proporciona una serie de operadores. Muchos de ellos son compatibles con los tipos integrados y permiten
realizar operaciones básicas con valores de esos tipos. Entre estos operadores se incluyen los siguientes grupos:
Operadores aritméticos, que realizan operaciones aritméticas con operandos numéricos.
Operadores de comparación, que comparan operandos numéricos.
Operadores lógicos booleanos, que realizan operaciones lógicas con operandos bool .
Operadores bit a bit y de desplazamiento, que realizan operaciones bit a bit o de desplazamiento con
operandos de tipos enteros.
Operadores de igualdad, que comprueban si sus operandos son iguales o no.
Normalmente, puede sobrecargar esos operadores, es decir, puede especificar el comportamiento del operador
para los operandos de un tipo definido por el usuario.
Las expresiones más simples de C# son literales (por ejemplo, números enteros y reales) y nombres de
variables. Puede combinarlas para crear expresiones complejas mediante el uso de operadores. La precedencia y
la asociatividad de los operadores determinan el orden en el que se realizan las operaciones en una expresión.
Puede usar los paréntesis para cambiar el orden de evaluación impuesto por la prioridad y la asociatividad de
operadores.
En el código siguiente, se muestran ejemplos de expresiones en el lado derecho de las asignaciones:
int a, b, c;
a = 7;
b = a;
c = b++;
b = a + b * c;
c = a >= 100 ? b : c / 10;
a = (int)Math.Sqrt(b * b + c * c);
Normalmente, una expresión genera un resultado que se puede incluir en otra expresión. Un método de
llamada void es un ejemplo de expresión que no genera un resultado. Solo se puede usar como instrucción, tal
como se muestra en el ejemplo siguiente:
Console.WriteLine("Hello, world!");
int[] numbers = { 2, 3, 4, 5 };
var maximumSquare = numbers.Max(x => x * x);
Console.WriteLine(maximumSquare);
// Output:
// 25
Puede usar una definición de cuerpo de expresiones para proporcionar una definición concisa para un método,
un constructor, una propiedad, un indexador o un finalizador.
Prioridad de operadores
En una expresión con varios operadores, los operadores con mayor prioridad se evalúan antes que los
operadores con menor prioridad. En el ejemplo siguiente, la multiplicación se realiza primero porque tiene
mayor prioridad que la suma:
var a = 2 + 2 * 2;
Console.WriteLine(a); // output: 6
Use paréntesis para cambiar el orden de evaluación impuesto por la prioridad de los operadores:
var a = (2 + 2) * 2;
Console.WriteLine(a); // output: 8
En la tabla siguiente se muestran los operadores de C# desde la precedencia más alta a la más baja. Los
operadores de cada fila comparten la prioridad.
x.y, f(x), a[i], x?.y , x?[y] , x++, x--, x!, new, typeof, Principal
checked, unchecked, default, nameof, delegate, sizeof,
stackalloc, x->y
+x, -x, !x, ~x, ++x, --x, ^x, (T)x, await, &x, *x, true and false Unario
O P ERA DO RES C AT EGO RÍA O N O M B RE
x..y Intervalo
x * y, x / y, x % y Multiplicativo
x + y, x – y Aditivo
x == y, x != y Igualdad
x || y OR condicional
Use paréntesis, para cambiar el orden de evaluación impuesto por la asociatividad de los operadores:
int a = 13 / 5 / 2;
int b = 13 / (5 / 2);
Console.WriteLine($"a = {a}, b = {b}"); // output: a = 1, b = 6
Evaluación de operandos
Independientemente de la prioridad y la asociatividad de los operadores, los operandos de una expresión se
evalúan de izquierda a derecha. En los siguientes ejemplos, se muestra el orden en el que se evalúan los
operadores y los operandos:
a + b a, b, +
a + b * c a, b, c, *, +
a / b + c * d a, b, /, c, d, *, +
a / (b + c) * d a, b, c, +, /, d, *
Normalmente, se evalúan todos los operandos de un operador. Sin embargo, algunos operadores evalúan los
operandos de forma condicional. Esto significa que el valor del operando situado más a la izquierda de este tipo
de operador define si se deben evaluar otros operandos, o bien qué operandos deben evaluarse. Estos
operadores son los operadores lógicos condicionales AND ( && ) y OR ( || ), los operadores de integración nula
?? y ??= , los operadores condicionales nulos ?. y ?[] , así como el operador condicional ?: . Para más
información, consulte la descripción de cada operador.
Vea también
Referencia de C#
Sobrecarga de operadores
Árboles de expresión
Operadores aritméticos (referencia de C#)
16/09/2021 • 10 minutes to read
Los operadores siguientes realizan operaciones aritméticas con operandos de tipos numéricos:
Operadores unarios ++ (incremento), -- (decremento), + (más) y - (menos).
Operadores binarios * (multiplicación), / (división), % (resto), + (suma) y - (resta).
Estos operadores se admiten en todos los tipos numéricos enteros y de punto flotante.
En el caso de tipos enteros, dichos operadores (excepto los operadores ++ y -- ) se definen para los tipos int
, uint , long y ulong . Cuando los operandos son de otro tipo entero ( sbyte , byte , short , ushort o char ),
sus valores se convierten en el tipo int , que también es el tipo de resultado de una operación. Cuando los
operandos son de distintos tipos enteros o de punto flotante, sus valores se convierten al tipo contenedor más
cercano, si ese tipo existe. Para obtener más información, vea la sección Promociones numéricas de
Especificación del lenguaje C#. Los operadores ++ y -- se definen para todos los tipos numéricos enteros y de
punto flotante y el tipo char.
Operador de incremento ++
El operador de incremento unario ++ incrementa su operando en 1. El operando debe ser una variable, un
acceso de propiedad o un acceso de indexador.
El operador de incremento se admite en dos formas: el operador de incremento posfijo ( x++ ) y el operador de
incremento prefijo ( ++x ).
Operador de incremento de postfijo
El resultado de x++ es el valor de x antes de la operación, tal y como se muestra en el ejemplo siguiente:
int i = 3;
Console.WriteLine(i); // output: 3
Console.WriteLine(i++); // output: 3
Console.WriteLine(i); // output: 4
double a = 1.5;
Console.WriteLine(a); // output: 1.5
Console.WriteLine(++a); // output: 2.5
Console.WriteLine(a); // output: 2.5
Operador de decremento --
El operador de decremento unario -- disminuye su operando en 1. El operando debe ser una variable, un
acceso de propiedad o un acceso de indexador.
El operador de decremento se admite en dos formas: el operador de decremento posfijo ( x-- ) y el operador de
decremento prefijo ( --x ).
Operador de decremento de postfijo
El resultado de x-- es el valor de x antes de la operación, tal y como se muestra en el ejemplo siguiente:
int i = 3;
Console.WriteLine(i); // output: 3
Console.WriteLine(i--); // output: 3
Console.WriteLine(i); // output: 2
double a = 1.5;
Console.WriteLine(a); // output: 1.5
Console.WriteLine(--a); // output: 0.5
Console.WriteLine(a); // output: 0.5
Console.WriteLine(+4); // output: 4
Console.WriteLine(-4); // output: -4
Console.WriteLine(-(-4)); // output: 4
uint a = 5;
var b = -a;
Console.WriteLine(b); // output: -5
Console.WriteLine(b.GetType()); // output: System.Int64
Operador de multiplicación *
El operador de multiplicación * calcula el producto de sus operandos:
Operador de división /
El operador de división / divide el operando izquierdo entre el derecho.
División de enteros
Para los operandos de tipos enteros, el resultado del operador / es de un tipo entero y equivale al cociente de
los dos operandos redondeados hacia cero:
Console.WriteLine(13 / 5); // output: 2
Console.WriteLine(-13 / 5); // output: -2
Console.WriteLine(13 / -5); // output: -2
Console.WriteLine(-13 / -5); // output: 2
Para obtener el cociente de los dos operandos como número de punto flotante, use el tipo float , double o
decimal :
int a = 13;
int b = 5;
Console.WriteLine((double)a / b); // output: 2.6
Si uno de los operandos es decimal , otro operando no puede ser float ni double , ya que ni float ni double
se convierte de forma implícita a decimal . Debe convertir explícitamente el operando float o double al tipo
decimal . Para obtener más información sobre las conversiones entre tipos numéricos, consulte Conversiones
numéricas integradas.
Operador de resto %
El operador de resto % calcula el resto después de dividir el operando izquierdo entre el derecho.
Resto entero
En el caso de los operandos de tipos enteros, el resultado de a % b es el valor producido por a - (a / b) * b .
El signo de resto distinto de cero es el mismo que el del operando izquierdo, como se muestra en el ejemplo
siguiente:
Use el método Math.DivRem para calcular los resultados de la división de enteros y del resto.
Resto de punto flotante
En el caso de los operandos float y double , el resultado de x % y para x e y finitos es el valor z , de
modo que
el signo de z , si no es cero, es el mismo que el signo de x ;
el valor absoluto de z es el valor producido por |x| - n * |y| , donde n es el entero más grande posible
que sea menor o igual que |x| / |y| , y |x| e |y| son los valores absolutos de x e y , respectivamente.
NOTE
Este método de cálculo del resto es análogo al que se usa para los operandos enteros, pero difiere de la especificación
IEEE 754. Si necesita la operación de resto conforme con la especificación IEEE 754, use el método Math.IEEERemainder.
Para información sobre el comportamiento del operador % con operandos no finitos, vea la sección Operador
de resto de la Especificación del lenguaje C#.
En el caso de los operandos decimal , el operador de resto % es equivalente al operador de resto del tipo
System.Decimal.
En el ejemplo siguiente se muestra el comportamiento del operador de resto con operandos de punto flotante:
Operador de suma +
El operador de suma + calcula la suma de sus operandos:
También puede usar el operador + para la concatenación de cadenas y la combinación de delegados. Para
obtener más información, consulte Operadores + y += .
Operador de resta -
El operador de resta - resta el operando derecho del izquierdo:
También puede usar el operador - para la eliminación de delegados. Para obtener más información, consulte
Operadores - y -= .
Asignación compuesta
Para un operador binario op , una expresión de asignación compuesta con el formato
x op= y
es equivalente a
x = x op y
a -= 4;
Console.WriteLine(a); // output: 10
a *= 2;
Console.WriteLine(a); // output: 20
a /= 4;
Console.WriteLine(a); // output: 5
a %= 3;
Console.WriteLine(a); // output: 2
A causa de las promociones numéricas, el resultado de la operación op podría no ser convertible de forma
implícita en el tipo T de x . En tal caso, si op es un operador predefinido y el resultado de la operación es
convertible de forma explícita en el tipo T de x , una expresión de asignación compuesta con el formato
x op= y es equivalente a x = (T)(x op y) , excepto que x solo se evalúa una vez. En el ejemplo siguiente se
muestra ese comportamiento:
byte a = 200;
byte b = 100;
var c = a + b;
Console.WriteLine(c.GetType()); // output: System.Int32
Console.WriteLine(c); // output: 300
a += b;
Console.WriteLine(a); // output: 44
Los operadores aritméticos binarios son asociativos a la izquierda. Es decir, los operadores con el mismo nivel
de prioridad se evalúan de izquierda a derecha.
Use los paréntesis, () , para cambiar el orden de evaluación impuesto por la prioridad y la asociatividad de
operadores.
int a = int.MaxValue;
int b = 3;
Para los operandos del tipo decimal , el desbordamiento aritmético siempre inicia una excepción
OverflowException y la división por cero siempre inicia una excepción DivideByZeroException.
Errores de redondeo
Debido a las limitaciones generales de la representación de punto flotante de los números reales y la aritmética
de punto flotante, es posible que se produzcan errores de redondeo en los cálculos con tipos de punto flotante.
Es decir, es posible que el resultado de una expresión difiera del resultado matemático esperado. En el ejemplo
siguiente se muestran varios de estos casos:
double a = 0.1;
double b = 3 * a;
Console.WriteLine(b == 0.3); // output: False
Console.WriteLine(b - 0.3); // output: 5.55111512312578E-17
decimal c = 1 / 3.0m;
decimal d = 3 * c;
Console.WriteLine(d == 1.0m); // output: False
Console.WriteLine(d); // output: 0.9999999999999999999999999999
Para más información, vea los comentarios en las páginas de referencia de System.Double, System.Single o
System.Decimal.
Vea también
Referencia de C#
Operadores y expresiones de C#
System.Math
System.MathF
Valores numéricos en .NET
Operadores lógicos booleanos (referencia de C#)
16/09/2021 • 8 minutes to read
Los operandos siguientes realizan operaciones lógicas con los operandos bool:
Operador unario ! (negación lógica).
Operadores binarios & (AND lógico), | (OR lógico) y ^ (OR exclusivo lógico). Esos operadores siempre
evalúan ambos operandos.
Operadores binarios && (AND lógico condicional) y || (OR lógico condicional). Esos operadores evalúan el
operando derecho solo si es necesario.
En el caso de los operandos de los tipos numéricos enteros, los operadores & , | y ^ realizan operaciones
lógicas bit a bit. Para obtener más información, vea Operadores de desplazamiento y bit a bit.
A partir de C# 8.0, el operador ! de postfijo unario es un operador que permite un valor NULL.
bool SecondOperand()
{
Console.WriteLine("Second operand is evaluated.");
return true;
}
En el caso de los operandos de los tipos numéricos enteros, el operador ^ calcula el AND lógico bit a bit de sus
operandos.
Operador lógico OR |
El operador | calcula el operador OR lógico de sus operandos. El resultado de x | y es true si x o y se
evalúan como true . De lo contrario, el resultado es false .
El operador | evalúa ambos operandos, incluso aunque el izquierdo se evalúe como true , para que el
resultado de la operación sea true con independencia del valor del operando derecho.
En el ejemplo siguiente, el operando derecho del operador | es una llamada de método, que se realiza
independientemente del valor del operando izquierdo:
bool SecondOperand()
{
Console.WriteLine("Second operand is evaluated.");
return true;
}
El operador OR lógico condicional || también calcula el operador OR lógico de sus operandos, pero no evalúa
el operando derecho si el izquierdo se evalúa como true .
En el caso de los operandos de los tipos numéricos enteros, el operador | calcula el OR lógico bit a bit de sus
operandos.
bool SecondOperand()
{
Console.WriteLine("Second operand is evaluated.");
return true;
}
El operador AND lógico & también calcula el operador AND lógico de sus operandos, pero siempre evalúa
ambos operandos.
bool SecondOperand()
{
Console.WriteLine("Second operand is evaluated.");
return true;
}
El operador OR lógico | también calcula el operador OR lógico de sus operandos, pero siempre evalúa ambos
operandos.
X Y XEY X|Y
El comportamiento de esos operadores difiere del comportamiento típico del operador con tipos de valor que
aceptan valores NULL. Por lo general, un operador que se define para los operandos de un tipo de valor también
se puede usar con los operandos del tipo de valor que acepta valores NULL correspondientes. Este tipo de
operador genera null si alguno de sus operandos se evalúa como null . Sin embargo, los operadores & y |
pueden generar un valor no NULL incluso si uno de los operandos es null . Para más información sobre el
comportamiento de los operadores con tipos de valor que aceptan valores NULL, consulte la sección
Operadores de elevación del artículo Tipos de valor que aceptan valores NULL.
También puede usar los operadores ! y ^ con los operandos bool? , como se muestra en el ejemplo
siguiente:
Asignación compuesta
Para un operador binario op , una expresión de asignación compuesta con el formato
x op= y
es equivalente a
x = x op y
test |= true;
Console.WriteLine(test); // output: True
test ^= false;
Console.WriteLine(test); // output: True
NOTE
Los operadores lógicos condicionales && y || no admiten la asignación compuesta.
Prioridad de operadores
En la lista siguiente se ordenan los operadores lógicos desde la prioridad más alta a la más baja:
Operador de negación lógico !
Operador AND lógico &
Operador OR exclusivo lógico ^
Operador OR lógico |
Operador AND lógico condicional &&
Operador OR lógico condicional ||
Use los paréntesis, () , para cambiar el orden de evaluación impuesto por la prioridad de los operadores:
Console.WriteLine(true | true & false); // output: True
Console.WriteLine((true | true) & false); // output: False
Para obtener la lista completa de los operadores de C# ordenados por nivel de prioridad, vea la sección
Prioridad de operadores del artículo Operadores de C#.
Vea también
Referencia de C#
Operadores y expresiones de C#
Operadores de desplazamiento y bit a bit
Operadores de desplazamiento y bit a bit
(referencia de C#)
16/09/2021 • 8 minutes to read
Los operadores siguientes realizan operaciones de desplazamiento o bit a bit con operandos de los tipos
numéricos enteros o el tipo char:
Operador unario ~ (complemento bit a bit)
Operadores de desplazamiento binarios << (desplazamiento izquierdo) y >> (desplazamiento derecho)
Operadores binarios & (AND lógico), | (OR lógico) y ^ (OR exclusivo lógico)
Estos operadores se definen para los tipos int , uint , long y ulong . Cuando ambos operandos son de otro
tipo entero ( sbyte , byte , short , ushort o char ), sus valores se convierten en el tipo int , que también es el
tipo de resultado de una operación. Cuando los operandos son de tipo entero diferente, sus valores se
convierten en el tipo entero más cercano que contenga. Para obtener más información, vea la sección
Promociones numéricas de Especificación del lenguaje C#.
Los operadores & , | y ^ también se definen para los operandos de tipo bool . Para obtener más
información, vea Operadores lógicos booleanos.
Las operaciones de desplazamiento y bit a bit nunca producen desbordamiento y generan los mismos
resultados en contextos Checked y Unchecked.
uint a = 0b_0000_1111_0000_1111_0000_1111_0000_1100;
uint b = ~a;
Console.WriteLine(Convert.ToString(b, toBase: 2));
// Output:
// 11110000111100001111000011110011
También se puede usar el símbolo ~ para declarar finalizadores. Para obtener más información, vea
Finalizadores.
uint y = x << 4;
Console.WriteLine($"After: {Convert.ToString(y, toBase: 2)}");
// Output:
// Before: 11001001000000000000000000010001
// After: 10010000000000000000000100010000
Dado que los operadores de desplazamiento solo se definen para los tipos int , uint , long y ulong , el
resultado de una operación siempre contiene al menos 32 bits. Si el operando izquierdo es de otro tipo entero (
sbyte , byte , short , ushort o char ), su valor se convierte al tipo int , como se muestra en el ejemplo
siguiente:
byte a = 0b_1111_0001;
var b = a << 8;
Console.WriteLine(b.GetType());
Console.WriteLine($"Shifted byte: {Convert.ToString(b, toBase: 2)}");
// Output:
// System.Int32
// Shifted byte: 1111000100000000
Para obtener información sobre cómo el operando derecho del operador << define el recuento de
desplazamiento, vea la sección Recuento de desplazamiento de los operadores de desplazamiento.
uint x = 0b_1001;
Console.WriteLine($"Before: {Convert.ToString(x, toBase: 2), 4}");
uint y = x >> 2;
Console.WriteLine($"After: {Convert.ToString(y, toBase: 2), 4}");
// Output:
// Before: 1001
// After: 10
Las posiciones de bits vacíos de orden superior se establecen basándose en el tipo del operando izquierdo, tal
como se indica a continuación:
Si el operando izquierdo es de tipo int o long , el operador de desplazamiento a la derecha realiza un
desplazamiento aritmético: el valor del bit más significativo (el bit de signo) del operando izquierdo se
propaga a las posiciones de bits vacíos de orden superior. Es decir, las posiciones de bits vacíos de orden
superior se establecen en cero si el operando izquierdo no es negativo y, en caso de serlo, se establecen
en uno.
int a = int.MinValue;
Console.WriteLine($"Before: {Convert.ToString(a, toBase: 2)}");
int b = a >> 3;
Console.WriteLine($"After: {Convert.ToString(b, toBase: 2)}");
// Output:
// Before: 10000000000000000000000000000000
// After: 11110000000000000000000000000000
uint c = 0b_1000_0000_0000_0000_0000_0000_0000_0000;
Console.WriteLine($"Before: {Convert.ToString(c, toBase: 2), 32}");
uint d = c >> 3;
Console.WriteLine($"After: {Convert.ToString(d, toBase: 2), 32}");
// Output:
// Before: 10000000000000000000000000000000
// After: 10000000000000000000000000000
Para obtener información sobre cómo el operando derecho del operador >> define el recuento de
desplazamiento, vea la sección Recuento de desplazamiento de los operadores de desplazamiento.
uint a = 0b_1111_1000;
uint b = 0b_1001_1101;
uint c = a & b;
Console.WriteLine(Convert.ToString(c, toBase: 2));
// Output:
// 10011000
Para los operandos bool , el operador & calcula el AND lógico de sus operandos. El operador & unario es el
operador address-of.
uint a = 0b_1111_1000;
uint b = 0b_0001_1100;
uint c = a ^ b;
Console.WriteLine(Convert.ToString(c, toBase: 2));
// Output:
// 11100100
Para los operandos bool , el operador ^ calcula el OR exclusivo lógico de sus operandos.
Operador lógico OR |
El operador | calcula el OR lógico bit a bit de sus operandos enteros:
uint a = 0b_1010_0000;
uint b = 0b_1001_0001;
uint c = a | b;
Console.WriteLine(Convert.ToString(c, toBase: 2));
// Output:
// 10110001
Asignación compuesta
Para un operador binario op , una expresión de asignación compuesta con el formato
x op= y
es equivalente a
x = x op y
uint a = 0b_1111_1000;
a &= 0b_1001_1101;
Display(a); // output: 10011000
a |= 0b_0011_0001;
Display(a); // output: 10111001
a ^= 0b_1000_0000;
Display(a); // output: 111001
a <<= 2;
Display(a); // output: 11100100
a >>= 4;
Display(a); // output: 1110
A causa de las promociones numéricas, el resultado de la operación op podría no ser convertible de forma
implícita en el tipo T de x . En tal caso, si op es un operador predefinido y el resultado de la operación es
convertible de forma explícita en el tipo T de x , una expresión de asignación compuesta con el formato
x op= y es equivalente a x = (T)(x op y) , excepto que x solo se evalúa una vez. En el ejemplo siguiente se
muestra ese comportamiento:
byte x = 0b_1111_0001;
int b = x << 8;
Console.WriteLine($"{Convert.ToString(b, toBase: 2)}"); // output: 1111000100000000
x <<= 8;
Console.WriteLine(x); // output: 0
Prioridad de operadores
En la lista siguiente se ordenan los operadores de desplazamiento y bit a bit desde la prioridad más alta a la más
baja:
Operador de complemento bit a bit ~
Operadores de desplazamiento << y >>
Operador AND lógico &
Operador OR exclusivo lógico ^
Operador OR lógico |
Use los paréntesis, () , para cambiar el orden de evaluación impuesto por la prioridad de los operadores:
uint a = 0b_1101;
uint b = 0b_1001;
uint c = 0b_1010;
uint d1 = a | b & c;
Display(d1); // output: 1101
uint d2 = (a | b) & c;
Display(d2); // output: 1000
Para obtener la lista completa de los operadores de C# ordenados por nivel de prioridad, vea la sección
Prioridad de operadores del artículo Operadores de C#.
Si el tipo de x es int o uint , el recuento de desplazamiento viene definido por los cinco bits de orden
inferior del operando derecho. Es decir, el valor de desplazamiento se calcula a partir de count & 0x1F (o
count & 0b_1_1111 ).
Si el tipo de x es long o ulong , el recuento de desplazamiento viene definido por los seis bits de orden
inferior del operando derecho. Es decir, el valor de desplazamiento se calcula a partir de count & 0x3F (o
count & 0b_11_1111 ).
int a = 0b_0001;
Console.WriteLine($"{a} << {count1} is {a << count1}; {a} << {count2} is {a << count2}");
// Output:
// 1 << 1 is 2; 1 << 225 is 2
int b = 0b_0100;
Console.WriteLine($"{b} >> {count1} is {b >> count1}; {b} >> {count2} is {b >> count2}");
// Output:
// 4 >> 1 is 2; 4 >> 225 is 2
NOTE
Tal y como se muestra en el ejemplo anterior, el resultado de una operación de desplazamiento puede ser distinto de cero
incluso si el valor del operando derecho es mayor que el número de bits del operando izquierdo.
Vea también
Referencia de C#
Operadores y expresiones de C#
Operadores lógicos booleanos
Operadores de igualdad (referencia de C#)
16/09/2021 • 5 minutes to read
Operador de igualdad ==
El operador de igualdad == devuelve true si sus operandos son iguales; en caso contrario, devuelve false .
Igualdad entre tipos de valor
Los operandos de los tipos de valor integrados son iguales si sus valores son iguales:
int a = 1 + 2 + 3;
int b = 6;
Console.WriteLine(a == b); // output: True
char c1 = 'a';
char c2 = 'A';
Console.WriteLine(c1 == c2); // output: False
Console.WriteLine(c1 == char.ToLower(c2)); // output: True
NOTE
Para los operadores == , < , > , <= y >= , si cualquier operando no es un número (Double.NaN o Single.NaN), el
resultado del operador será false . Esto significa que el valor NaN no es mayor, inferior ni igual que cualquier otro valor
double o float , incluido NaN . Para obtener más información y ejemplos, vea el artículo de referencia Double.NaN o
Single.NaN.
Dos operandos del mismo tipo enum son iguales si los valores correspondientes del tipo entero subyacente son
iguales.
Los tipos struct definidos por el usuario no son compatibles con el operador == de forma predeterminada. Para
admitir el operador == , un elemento struct definido por el usuario debe sobrecargarlo.
A partir de C# 7.3, los operadores == y != son compatibles con las tuplas de C#. Si desea más información,
consulte la sección Igualdad de tupla del artículo Tipos de tupla.
Igualdad entre tipos de referencia
De forma predeterminada, dos operandos de tipo de referencia que no son registros son iguales si hacen
referencia al mismo objeto:
public class ReferenceTypesEquality
{
public class MyClass
{
private int id;
Como se muestra en el ejemplo, el operador == es compatible de forma predeterminada con los tipos de
referencia definidos por el usuario. Sin embargo, un tipo de referencia puede sobrecargar el operador == . Si un
tipo de referencia sobrecarga el operador == , use el método Object.ReferenceEquals para comprobar si dos
referencias de ese tipo hacen referencia al mismo objeto.
Igualdad entre tipos de registro
Disponibles en C# 9.0 y versiones posteriores, los tipos de registro admiten los operadores == y != que, de
forma predeterminada, proporcionan semántica de igualdad de valores. Es decir, dos operandos de registro son
iguales cuando ambos son null o los valores correspondientes de todos los campos y las propiedades
implementadas de forma automática son iguales.
Como se muestra en el ejemplo anterior, en el caso de los miembros de tipo de referencia que no son de
registro, se comparan sus valores de referencia, no las instancias a las que se hace referencia.
Igualdad entre cadenas
Dos operandos string son iguales si ambos son null , o bien si las instancias de ambas cadenas tienen la misma
longitud y los mismos caracteres en cada posición de caracteres:
string s1 = "hello!";
string s2 = "HeLLo!";
Console.WriteLine(s1 == s2.ToLower()); // output: True
string s3 = "Hello!";
Console.WriteLine(s1 == s3); // output: False
Es la comparación de ordinales que distingue entre mayúsculas de minúsculas. Para obtener más información
sobre la comparación de cadenas, vea Cómo comparar cadenas en C# .
Igualdad entre delegados
Dos operandos de delegado con el mismo tipo de entorno de ejecución son iguales cuando ambos son null ,o
bien cuando sus listas de invocación tienen la misma longitud y las mismas entradas en cada posición:
Action b = a + a;
Action c = a + a;
Console.WriteLine(object.ReferenceEquals(b, c)); // output: False
Console.WriteLine(b == c); // output: True
Para obtener más información, vea la sección sobre los operadores de igualdad entre delegados de la
Especificación del lenguaje C#.
Los delegados que se producen mediante la evaluación de expresiones lambda semánticamente idénticas no
son iguales, tal como se muestra en el ejemplo siguiente:
Operador de desigualdad !=
El operador de desigualdad ( != ) devuelve true si sus operandos son iguales; en caso contrario, devuelve
false . Para los operandos de los tipos integrados, la expresión x != y genera el mismo resultado que la
expresión !(x == y) . Para obtener más información sobre la igualdad de tipos, vea la sección Operador de
igualdad.
En el siguiente ejemplo se muestra el uso del operador != :
int a = 1 + 1 + 2 + 3;
int b = 6;
Console.WriteLine(a != b); // output: True
string s1 = "Hello";
string s2 = "Hello";
Console.WriteLine(s1 != s2); // output: False
object o1 = 1;
object o2 = 1;
Console.WriteLine(o1 != o2); // output: True
Posibilidad de sobrecarga del operador
Un tipo definido por el usuario puede sobrecargar los operadores == y != . Si un tipo sobrecarga uno de los
dos operadores, también debe sobrecargar el otro.
Un tipo de registro no puede sobrecargar de forma explícita los operadores == y != . Si tiene que cambiar el
comportamiento de los operadores == y != para el tipo de registro T , implemente el método
IEquatable<T>.Equals con la signatura siguiente:
Vea también
Referencia de C#
Operadores y expresiones de C#
System.IEquatable<T>
Object.Equals
Object.ReferenceEquals
Comparaciones de igualdad
Operadores de comparación
Operadores de comparación (referencia de C#)
16/09/2021 • 2 minutes to read
Los operadores de la comparación < (menor que), > (mayor que), <= (menor o igual que) y >= (mayor o
igual que), también conocidos como relacionales, comparan sus operandos. Estos operadores se admiten en
todos los tipos numéricos enteros y de punto flotante.
NOTE
Para los operadores == , < , > , <= y >= , si cualquier operando no es un número (Double.NaN o Single.NaN), el
resultado del operador será false . Esto significa que el valor NaN no es mayor, inferior ni igual que cualquier otro valor
double o float , incluido NaN . Para obtener más información y ejemplos, vea el artículo de referencia Double.NaN o
Single.NaN.
El tipo char también admite operadores de comparación. En el caso de los operandos char , se comparan los
códigos de caracteres correspondientes.
Los tipos de enumeración también admiten operadores de comparación. Para los operandos del mismo tipo
enum, se comparan los valores correspondientes del tipo entero subyacente.
Los operadores == y != comprueban si los operandos son iguales.
Vea también
Referencia de C#
Operadores y expresiones de C#
System.IComparable<T>
Operadores de igualdad
Operadores y expresiones de acceso a miembros
(referencia de C#)
16/09/2021 • 9 minutes to read
Puede usar los operadores y expresiones siguientes cuando accede a un miembro de tipo:
. (acceso a miembros): para acceder a un miembro de un espacio de nombres o un tipo
[] (elemento de matriz o acceso a indizador): para acceder a un elemento de matriz o un indizador de tipo
?. y ?[] (operadores condicionales NULL): para realizar una operación de acceso a elementos o miembros
solo si un operando es distinto de NULL
() (invocación): para llamar a un método de acceso o invocar un delegado
^ (índice desde el final) : para indicar que la posición del elemento se encuentra a partir del final de una
secuencia
.. (intervalo): para especificar un intervalo de índices que puede utilizar para obtener un intervalo de
elementos de secuencia
using System.Collections.Generic;
Use . para formar un nombre completo para tener acceso a un tipo dentro de un espacio de nombres,
como se muestra en el código siguiente:
Utilice una directiva using para hacer que el uso de nombres completos sea opcional.
Use . para tener acceso a los miembros de tipo, que no son estáticos y, como se muestra en el código
siguiente:
Si un índice de matriz se encuentra fuera de los límites de la dimensión correspondiente de una matriz, se
produce una excepción IndexOutOfRangeException.
Tal como se muestra en el ejemplo anterior, también usa corchetes al declarar un tipo de matriz o crear
instancias de matriz.
Para obtener más información sobre las matrices, consulte Matrices.
Acceso a indizador
En el ejemplo siguiente se usa el tipo Dictionary<TKey,TValue> de .NET para mostrar el acceso al indizador:
Los indizadores le permiten indizar las instancias de un tipo definido por el usuario de un modo similar a la
indización de matrices. A diferencia de los índices de matriz, que deben ser enteros, los parámetros de indizador
se pueden declarar para ser de cualquier tipo.
Para más información sobre los indizadores, consulte Indizadores.
Otros usos de []
Para información sobre el acceso de los elementos de puntero, consulte la sección Operador de acceso de
elemento de puntero del artículo Operadores relacionados con el puntero.
También usa los corchetes para especificar atributos:
[System.Diagnostics.Conditional("DEBUG")]
void TraceMethod() {}
NOTE
Si a.x o a[x] producen una excepción, a?.x o a?[x] produciría la misma excepción para a no NULL. Por
ejemplo, si a es una instancia de matriz que no es NULL y x está fuera de los límites de a , a?[x] produciría
una excepción IndexOutOfRangeException.
Los operadores de condición NULL se cortocircuitan. Es decir, si una operación en una cadena de la operación de
acceso a elementos o miembros condicional devuelve null , no se ejecuta el resto de la cadena. En el ejemplo
siguiente, B no se evalúa si A se evalúa como null y C no se evalúa si A o B se evalúan como null :
A?.B?.Do(C);
A?.B?[C];
Si A podría ser NULL, pero B y C no lo serían si A no lo es también, solo tiene que aplicar el operador
condicional NULL a A :
A?.B.C();
namespace MemberAccessOperators2
{
public static class NullConditionalShortCircuiting
{
public static void Main()
{
Person person = null;
person?.Name.Write(); // no output: Write() is not called due to short-circuit.
try
{
(person?.Name).Write();
}
catch (NullReferenceException)
{
Console.WriteLine("NullReferenceException");
}; // output: NullReferenceException
}
}
En el primero de los dos ejemplos anteriores también se usa el operador de fusión de NULL ?? para especificar
una expresión alternativa que se evaluará en caso de que el resultado de la operación condicional NULL sea
null .
Si a.x o a[x] es de un tipo de valor que no admite un valor NULL, T , a?.x o a?[x] es del tipo de valor que
admite un valor NULL T? correspondiente. Si necesita una expresión de tipo T , aplique el operador de fusión
de NULL ?? a una expresión condicional NULL, tal como se muestra en el ejemplo siguiente:
Console.WriteLine(GetSumOfFirstTwoOrDefault(null)); // output: 0
Console.WriteLine(GetSumOfFirstTwoOrDefault(new int[0])); // output: 0
Console.WriteLine(GetSumOfFirstTwoOrDefault(new[] { 3, 4, 5 })); // output: 7
En el ejemplo anterior, si no utiliza el operador ?? , numbers?.Length < 2 da como resultado false cuando
numbers es null .
El operador de acceso de miembro condicional NULL ?. también se conoce con el nombre de operador Elvis.
Invocación de delegado seguro para subprocesos
Use el operador ?. para comprobar si un delegado es distinto de NULL y se invoca de forma segura para
subprocesos (por ejemplo, cuando se genera un evento), tal como se muestra en el código siguiente:
PropertyChanged?.Invoke(…)
Ese código es equivalente al código siguiente que se usaría en C# 5 o una versión anterior:
Es un modo seguro para subprocesos de asegurarse de que solo se invoca un handler que no es NULL. Dado
que las instancias de delegado son inmutables, ningún subproceso puede cambiar el valor al que hace
referencia la variable local handler . En concreto, si el código que ha ejecutado otro subproceso cancela la
suscripción del evento PropertyChanged y PropertyChanged se convierte en null antes de que se invoque
handler , el objeto al que hace referencia handler queda intacto. El operador ?. evalúa el operando de la
izquierda no más de una vez, lo que garantiza que no se pueda cambiar a null después de verificarse como no
NULL.
Expresión de invocación ()
Utilice paréntesis, () , para llamar a un método o invocar un delegado.
En el ejemplo siguiente se muestra cómo llamar a un método, con o sin argumentos, y cómo invocar un
delegado:
numbers.Clear();
display(numbers.Count); // output: 0
Operador de intervalo .
Disponible en C# 8.0 y versiones posteriores, el operador .. especifica el inicio y el final de un intervalo de
índices como sus operandos. El operando izquierdo es un inicio inclusivo de un intervalo. El operando derecho
es un inicio exclusivo de un intervalo. Cualquiera de los operandos puede ser un índice desde el inicio o desde el
final de una secuencia, tal y como muestra el ejemplo siguiente:
int margin = 1;
int[] inner = numbers[margin..^margin];
Display(inner); // output: 10 20 30 40
Como se muestra en el ejemplo anterior, la expresión a..b es del tipo System.Range. En la expresión a..b , los
resultados de a y b deben poderse convertir implícitamente a int o Index.
Puede omitir cualquiera de los operandos del operador .. para obtener un intervalo abierto:
a.. es equivalente a a..^0
..b es equivalente a 0..b
.. es equivalente a 0..^0
int[] numbers = new[] { 0, 10, 20, 30, 40, 50 };
int amountToDrop = numbers.Length / 2;
Vea también
Referencia de C#
Operadores y expresiones de C#
?? (operador de uso combinado de NULL)
Operador ::
Operadores de prueba de tipos y expresión de
conversión (referencia de C#)
16/09/2021 • 6 minutes to read
Puede usar los siguientes operadores y expresiones para realizar la comprobación de tipos o la conversión de
tipos:
operador is: para comprobar si el tipo en tiempo de ejecución de una expresión es compatible con un tipo
determinado.
operador as: para convertir explícitamente una expresión a un tipo determinado si su tipo en tiempo de
ejecución es compatible con ese tipo.
expresión Cast: para realizar una conversión explícita.
operador typeof: para obtener la instancia System.Type para un tipo.
Operador is
El operador is comprueba si el tipo en tiempo de ejecución del resultado de una expresión es compatible con
un tipo determinado. A partir de C# 7.0, el operador is también prueba el resultado de una expresión en
relación con un patrón.
La expresión con el operador is de prueba de tipos tiene el formato siguiente:
E is T
donde E es una expresión que devuelve un valor y T es el nombre de un tipo o un parámetro de tipo. E no
puede ser un método anónimo ni una expresión lambda.
El operador is devuelve true cuando el resultado de una expresión es distinto de NULL y se cumple
cualquiera de las condiciones siguientes:
El tipo en tiempo de ejecución del resultado de una expresión es T .
El tipo en tiempo de ejecución del resultado de una expresión deriva del tipo T , implementa una interfaz
T , o bien otra conversión de referencia implícita existe en T .
El tipo en tiempo de ejecución del resultado de una expresión es un tipo de valor que admite un valor
NULL con el tipo subyacente T y Nullable<T>.HasValue es true .
Existe una conversión boxing o unboxing del tipo en tiempo de ejecución del resultado de una expresión
al tipo T .
El operador is no toma en consideración las conversiones definidas por el usuario.
En el ejemplo siguiente se muestra que el operador is devuelve true si el tipo en tiempo de ejecución del
resultado de una expresión se deriva de un tipo determinado, es decir, existe una conversión de referencia entre
tipos:
public class Base { }
En el ejemplo siguiente se muestra que el operador is tiene en cuenta las conversiones boxing y unboxing
pero no considera las conversiones numéricas:
int i = 27;
Console.WriteLine(i is System.IFormattable); // output: True
object iBoxed = i;
Console.WriteLine(iBoxed is int); // output: True
Console.WriteLine(iBoxed is long); // output: False
Para obtener información acerca de las conversiones de C#, vea el capítulo Conversiones de la especificación del
lenguaje C#.
Prueba de tipos con coincidencia de patrones
A partir de C# 7.0, el operador is también prueba el resultado de una expresión en relación con un patrón. En
el ejemplo siguiente se muestra cómo usar un patrón de declaración para comprobar el tipo en tiempo de
ejecución de una expresión:
int i = 23;
object iBoxed = i;
int? jNullable = 7;
if (iBoxed is int a && jNullable is int b)
{
Console.WriteLine(a + b); // output 30
}
Operador as
El operador as convierte explícitamente el resultado de una expresión en una referencia determinada o un tipo
de valor que acepta valores NULL. Si la conversión no es posible, el operador as devuelve null . A diferencia
de la expresión Cast, el operador as no genera nunca una excepción.
La expresión con el formato
E as T
donde E es una expresión que devuelve un valor y T es el nombre de un tipo o un parámetro de tipo produce
el mismo resultado que
E is T ? (T)(E) : (T)null
NOTE
Como se muestra en el ejemplo anterior, se necesita comparar el resultado de la expresión as con null para
comprobar si una conversión es correcta. A partir de C# 7.0, puede usar el operador is para probar si la conversión es
correcta y, si es así, asignar su resultado a una nueva variable.
Expresión Cast
Una expresión de conversión con el formato (T)E realiza una conversión explícita del resultado de la expresión
E al tipo T . Si no existe ninguna conversión explícita del tipo de E al tipo T , se producirá un error en tiempo
de compilación. En el tiempo de ejecución, una conversión explícita podría no completarse correctamente y una
expresión de conversión podría generar una excepción.
El ejemplo siguiente muestra las conversiones explícitas numérica y de referencia:
double x = 1234.7;
int a = (int)x;
Console.WriteLine(a); // output: 1234
Para obtener más información sobre las conversiones explícitas, vea la sección Conversiones explícitas de la
especificación del lenguaje C#. Para obtener información sobre cómo definir una conversión personalizada de
tipo explícito o implícito, vea Operadores de conversión definidos por el usuario.
Otros usos de ()
También puede utilizar paréntesis para llamar a un método o invocar un delegado.
Sirven además para ajustar el orden en el que se van a evaluar operaciones en una expresión. Para obtener más
información, vea Operadores de C# (referencia de C#).
typeof (operador)
El operador typeof obtiene la instancia System.Type para un tipo. El argumento del operador typeof debe ser
el nombre de un tipo o un parámetro de tipo, como se muestra en el ejemplo siguiente:
Console.WriteLine(typeof(List<string>));
PrintType<int>();
PrintType<System.Int32>();
PrintType<Dictionary<int, char>>();
// Output:
// System.Collections.Generic.List`1[System.String]
// System.Int32
// System.Int32
// System.Collections.Generic.Dictionary`2[System.Int32,System.Char]
También se puede usar el operador typeof con tipos genéricos sin enlazar. El nombre de un tipo genérico sin
enlazar debe contener el número apropiado de comas, que es inferior en una unidad al número de parámetros
de tipo. En el siguiente ejemplo se muestra el uso del operador typeof con un tipo genérico sin enlazar:
Console.WriteLine(typeof(Dictionary<,>));
// Output:
// System.Collections.Generic.Dictionary`2[TKey,TValue]
Una expresión no puede ser un argumento del operador typeof . Para obtener la instancia de System.Type para
el tipo en tiempo de ejecución del resultado de una expresión, use el método Object.GetType.
Prueba de tipos con el operador typeof
Use el operador typeof para comprobar si el tipo en tiempo de ejecución del resultado de la expresión coincide
exactamente con un tipo determinado. En el ejemplo siguiente se muestra la diferencia entre la comprobación
de tipos realizada con el operador typeof y el operador is:
Vea también
Referencia de C#
Operadores y expresiones de C#
Procedimiento para convertir de forma segura mediante la coincidencia de patrones y los operadores is y as
Elementos genéricos en .NET
Operadores de conversión definidos por el usuario
(referencia de C#)
16/09/2021 • 2 minutes to read
Un tipo definido por el usuario puede definir una conversión implícita o explícita personalizada desde o a otro
tipo.
Las conversiones implícitas no requieren que se invoque una sintaxis especial y pueden producirse en diversas
situaciones, por ejemplo, en las invocaciones de métodos y asignaciones. Las conversiones implícitas
predefinidas en C# siempre se realizan correctamente y nunca inician una excepción. Las conversiones implícitas
definidas por el usuario deben comportarse de esta manera. Si una conversión personalizada puede producir
una excepción o perder información, se define como una conversión explícita.
Los operadores is y as no tienen en cuenta las conversiones definidas por el usuario. Use una expresión Cast
para invocar una conversión explícita definida por el usuario.
Use las palabras clave operator y implicit o explicit para definir una conversión implícita o explícita,
respectivamente. El tipo que define una conversión debe ser un tipo de origen o un tipo de destino de dicha
conversión. Una conversión entre dos tipos definidos por el usuario se puede definir en cualquiera de los dos
tipos.
En el ejemplo siguiente se muestra cómo se define una conversión implícita y explícita:
using System;
byte number = d;
Console.WriteLine(number); // output: 7
Use también la palabra clave operator para sobrecargar un operador predefinido en C#. Para obtener más
información, vea Sobrecarga de operadores.
Vea también
Referencia de C#
Operadores y expresiones de C#
Sobrecarga de operadores
Operadores de conversión y prueba de tipos
Conversiones de tipos
Directrices de diseño: operadores de conversión
Chained user-defined explicit conversions in C# (Conversiones explícitas encadenadas definidas por el
usuario en C#)
Operadores relacionados con el puntero (referencia
de C#)
16/09/2021 • 8 minutes to read
NOTE
Cualquier operación con punteros requiere un contexto unsafe. El código que contenga bloques no seguros se tendrá que
compilar con la opción del compilador AllowUnsafeBlocks .
unsafe
{
int number = 27;
int* pointerToNumber = &number;
El operando del operador & debe ser una variable fija. Las variables fijas son las que residen en ubicaciones de
almacenamiento que no se ven afectadas por el funcionamiento del recolector de elementos no utilizados. En el
ejemplo anterior, la variable local number es una variable fija, ya que reside en la pila. Las variables que residen
en ubicaciones de almacenamiento que pueden verse afectadas por el recolector de elementos no utilizados
(por ejemplo, reubicadas) se denominan variables móviles. Los campos de objeto y los elementos de matriz son
ejemplos de variables móviles. Puede obtener la dirección de una variable móvil si la "fija" o "ancla" con una
instrucción fixed . La dirección obtenida solo es válida dentro del bloque de una instrucción fixed . En el
ejemplo siguiente se muestra cómo usar una instrucción fixed y el operador & :
unsafe
{
byte[] bytes = { 1, 2, 3 };
fixed (byte* pointerToFirst = &bytes[0])
{
// The address stored in pointerToFirst
// is valid only inside this fixed statement block.
}
}
unsafe
{
char letter = 'A';
char* pointerToLetter = &letter;
Console.WriteLine($"Value of the `letter` variable: {letter}");
Console.WriteLine($"Address of the `letter` variable: {(long)pointerToLetter:X}");
*pointerToLetter = 'Z';
Console.WriteLine($"Value of the `letter` variable after update: {letter}");
}
// Output is similar to:
// Value of the `letter` variable: A
// Address of the `letter` variable: DCB977DDF4
// Value of the `letter` variable after update: Z
x->y
es equivalente a
(*x).y
unsafe
{
char* pointerToChars = stackalloc char[123];
NOTE
El operador de acceso de elemento de puntero no busca errores fuera de límites.
No puede usar [] para el acceso de elemento de puntero con una expresión de tipo void* .
También puede usar el operador [] para acceso de elemento de matriz o indizador.
unsafe
{
const int Count = 3;
int[] numbers = new int[Count] { 10, 20, 30 };
fixed (int* pointerToFirst = &numbers[0])
{
int* pointerToLast = pointerToFirst + (Count - 1);
Resta de puntero
En el caso de dos punteros p1 y p2 de tipo T* , la expresión p1 - p2 genera la diferencia entre las
direcciones proporcionadas por p1 y p2 dividida por sizeof(T) . El tipo del resultado es long . Es decir,
p1 - p2 se calcula como ((long)(p1) - (long)(p2)) / sizeof(T) .
unsafe
{
int* numbers = stackalloc int[] { 0, 1, 2, 3, 4, 5 };
int* p1 = &numbers[1];
int* p2 = &numbers[5];
Console.WriteLine(p2 - p1); // output: 4
}
unsafe
{
int* numbers = stackalloc int[] { 0, 1, 2 };
int* p1 = &numbers[0];
int* p2 = p1;
Console.WriteLine($"Before operation: p1 - {(long)p1}, p2 - {(long)p2}");
Console.WriteLine($"Postfix increment of p1: {(long)(p1++)}");
Console.WriteLine($"Prefix increment of p2: {(long)(++p2)}");
Console.WriteLine($"After operation: p1 - {(long)p1}, p2 - {(long)p2}");
}
// Output is similar to
// Before operation: p1 - 816489946512, p2 - 816489946512
// Postfix increment of p1: 816489946512
// Prefix increment of p2: 816489946516
// After operation: p1 - 816489946516, p2 - 816489946516
Prioridad de operadores
En la lista siguiente se ordenan los operadores relacionados con el puntero desde la prioridad más alta a la más
baja:
Operadores de incremento x++ y decremento x-- postfijos y operadores -> y []
Operadores de incremento ++x y decremento --x prefijos y operadores & y *
Operadores + y - de suma
Operadores de comparación < , > , <= y >=
Operadores de igualdad == y !=
Use paréntesis, () , para cambiar el orden de evaluación impuesto por la prioridad de los operadores.
Para obtener la lista completa de los operadores de C# ordenados por nivel de prioridad, vea la sección
Prioridad de operadores del artículo Operadores de C#.
Vea también
Referencia de C#
Operadores y expresiones de C#
Tipos de puntero
unsafe (palabra clave)
fixed (palabra clave)
stackalloc
sizeof (operador)
Operadores de asignación (referencia de C#)
16/09/2021 • 2 minutes to read
El operador de asignación = asigna el valor de su operando de la derecha a una variable, una propiedad o un
elemento de indizador proporcionado por el operando de la izquierda. El resultado de una expresión de
asignación es el valor asignado al operando izquierdo. El tipo del operando de la derecha debe ser el mismo que
el del operando de la izquierda o debe poder convertirse implícitamente en él.
El operador de asignación = es asociativo a la derecha, es decir, una expresión con el formato
a = b = c
se evalúa como
a = (b = c)
En el ejemplo siguiente se muestra el uso del operador de asignación con una variable local, una propiedad y un
elemento indexador como su operando izquierdo:
Console.WriteLine(numbers.Capacity);
numbers.Capacity = 100;
Console.WriteLine(numbers.Capacity);
// Output:
// 4
// 100
int newFirstElement;
double originalFirstElement = numbers[0];
newFirstElement = 5;
numbers[0] = newFirstElement;
Console.WriteLine(originalFirstElement);
Console.WriteLine(numbers[0]);
// Output:
// 1
// 5
En el caso del operador de asignación de referencias, sus dos operandos deben ser del mismo tipo.
Asignación compuesta
Para un operador binario op , una expresión de asignación compuesta con el formato
x op= y
es equivalente a
x = x op y
Vea también
Referencia de C#
Operadores y expresiones de C#
ref (palabra clave)
Expresiones lambda (referencia de C#)
16/09/2021 • 12 minutes to read
Use una expresión lambda para crear una función anónima. Use el operador de declaración lambda => para
separar la lista de parámetros de la lamba de su cuerpo. Una expresión lambda puede tener cualquiera de las
dos formas siguientes:
Una lambda de expresión que tiene una expresión como cuerpo:
Para crear una expresión lambda, especifique los parámetros de entrada (si existen) a la izquierda del operador
lambda y una expresión o bloque de instrucciones en el otro lado.
Toda expresión lambda se puede convertir en un tipo delegado. El tipo delegado al que se puede convertir una
expresión lambda se define según los tipos de sus parámetros y el valor devuelto. Si una expresión lambda no
devuelve un valor, se puede convertir en uno de los tipos delegados Action ; de lo contrario, se puede convertir
en uno de los tipos delegados Func . Por ejemplo, una expresión lambda que tiene dos parámetros y no
devuelve ningún valor corresponde a un delegado Action<T1,T2>. Una expresión lambda que tiene un
parámetro y devuelve un valor se puede convertir en un delegado Func<T,TResult>. En el ejemplo siguiente, la
expresión lambda x => x * x , que especifica un parámetro denominado x y devuelve el valor de x al
cuadrado, se asigna a una variable de un tipo delegado:
Las expresiones lambda también se pueden convertir en los tipos de árbol de expresión, como se muestra en los
ejemplos siguientes:
Puede usar expresiones lambda en cualquier código que requiera instancias de tipos delegados o de árboles de
expresión, por ejemplo, como un argumento del método Task.Run(Action) para pasar el código que se debe
ejecutar en segundo plano. También puede usar expresiones lambda al escribir LINQ en C#, como se muestra en
el ejemplo siguiente:
int[] numbers = { 2, 3, 4, 5 };
var squaredNumbers = numbers.Select(x => x * x);
Console.WriteLine(string.Join(" ", squaredNumbers));
// Output:
// 4 9 16 25
Lambdas de expresión
Una expresión lambda con una expresión en el lado derecho del operador => se denomina lambda de
expresión. Una expresión lambda devuelve el resultado de evaluar la condición y tiene la siguiente forma:
El cuerpo de una expresión lambda puede constar de una llamada al método. Pero si crea árboles de expresión
que se evalúan fuera del contexto de Common Language Runtime (CLR) de .NET, como en SQL Server, no debe
usar llamadas a métodos en expresiones lambda. Los métodos no tendrán ningún significado fuera del contexto
de Common Language Runtime (CLR) de .NET.
Lambdas de instrucción
Una lambda de instrucción es similar a un lambda de expresión, salvo que las instrucciones se encierran entre
llaves:
El cuerpo de una lambda de instrucción puede estar compuesto de cualquier número de instrucciones; sin
embargo, en la práctica, generalmente no hay más de dos o tres.
Si una expresión lambda solo tiene un parámetro de entrada, los paréntesis son opcionales:
A veces, el compilador no puede deducir los tipos de parámetros de entrada. Puede especificar los tipos de
manera explícita, tal como se muestra en el ejemplo siguiente:
Los tipos de parámetro de entrada deben ser todos explícitos o todos implícitos; de lo contrario, se produce un
error del compilador CS0748.
A partir de C# 9.0, puede usar descartes para especificar dos o más parámetros de entrada de una expresión
lambda que no se usan en la expresión:
Los parámetros de descarte lambda pueden ser útiles cuando se usa una expresión lambda para proporcionar
un controlador de eventos.
NOTE
Por compatibilidad con versiones anteriores, si solo un parámetro de entrada se denomina _ , dentro de una expresión
lambda, _ se trata como el nombre de ese parámetro.
Lambdas asincrónicas
Puede crear fácilmente expresiones e instrucciones lambda que incorporen el procesamiento asincrónico
mediante las palabras clave async y await . Por ejemplo, en el siguiente ejemplo de formularios Windows Forms
se incluye un controlador de eventos que llama y espera un método asincrónico, ExampleMethodAsync .
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
button1.Click += button1_Click;
}
Puede agregar el mismo controlador de eventos utilizando una lambda asincrónica. Para agregar este
controlador, agregue un modificador async antes de la lista de parámetros lambda, como se muestra en el
ejemplo siguiente:
Para obtener más información sobre cómo crear y usar métodos asincrónicos, vea Programación asincrónica
con async y await.
Normalmente, los campos de una tupla se denominan Item1 , Item2 , etc., aunque puede definir una tupla con
componentes con nombre, como en el ejemplo siguiente.
Func<(int n1, int n2, int n3), (int, int, int)> doubleThem = ns => (2 * ns.n1, 2 * ns.n2, 2 * ns.n3);
var numbers = (2, 3, 4);
var doubledNumbers = doubleThem(numbers);
Console.WriteLine($"The set {numbers} doubled: {doubledNumbers}");
Para más información sobre las tuplas de C#, consulte el artículo sobre los tipos de tuplas.
Se pueden crear instancias del delegado como una instancia Func<int, bool> , donde int es un parámetro de
entrada y bool es el valor devuelto. El valor devuelto siempre se especifica en el último parámetro de tipo. Por
ejemplo, Func<int, string, bool> define un delegado con dos parámetros de entrada, int y string , y un tipo
de valor devuelto de bool . El delegado Func siguiente, cuando se invoca, devuelve un valor booleano que
indica si el parámetro de entrada es igual a cinco:
También puede proporcionar una expresión lambda cuando el tipo de argumento es Expression<TDelegate>,
(por ejemplo, en los operadores de consulta estándar que se definen en el tipo Queryable). Al especificar un
argumento Expression<TDelegate>, la lambda se compila en un árbol de expresión.
En el ejemplo siguiente se usa el operador de consulta estándar Count:
int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
int oddNumbers = numbers.Count(n => n % 2 == 1);
Console.WriteLine($"There are {oddNumbers} odd numbers in {string.Join(" ", numbers)}");
El compilador puede deducir el tipo del parámetro de entrada o también se puede especificar explícitamente.
Esta expresión lambda particular cuenta aquellos enteros ( n ) que divididos por dos dan como resto 1.
El siguiente ejemplo genera una secuencia que contiene todos los elementos de la matriz numbers que
preceden al 9, ya que ese es el primer número de la secuencia que no cumple la condición:
int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
var firstNumbersLessThanSix = numbers.TakeWhile(n => n < 6);
Console.WriteLine(string.Join(" ", firstNumbersLessThanSix));
// Output:
// 5 4 1 3
En el siguiente ejemplo se especifican varios parámetros de entrada encerrándolos entre paréntesis. El método
devuelve todos los elementos de la matriz numbers hasta que encuentra un número cuyo valor es menor que la
posición ordinal en la matriz:
int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
var firstSmallNumbers = numbers.TakeWhile((n, index) => n >= index);
Console.WriteLine(string.Join(" ", firstSmallNumbers));
// Output:
// 5 4
Las reglas generales para la inferencia de tipos de las lambdas son las siguientes:
La lambda debe contener el mismo número de parámetros que el tipo delegado.
Cada parámetro de entrada de la lambda debe poder convertirse implícitamente a su parámetro de
delegado correspondiente.
El valor devuelto de la lambda (si existe) debe poder convertirse implícitamente al tipo de valor devuelto
del delegado.
Observe que las expresiones lambda, en sí mismas, no tienen tipo, ya que el sistema de tipos comunes no tiene
ningún concepto intrínseco de "expresión lambda". Sin embargo, a veces resulta práctico hablar coloquialmente
del "tipo" de una expresión lambda. En estos casos, el tipo hace referencia al tipo del delegado o el tipo de
Expression en el que se convierte la expresión lambda.
updateCapturedLocalVariable = x =>
{
j = x;
bool result = j > input;
Console.WriteLine($"{j} is greater than {input}: {result}");
};
isEqualToCapturedLocalVariable = x => x == j;
int gameInput = 5;
game.Run(gameInput);
int anotherJ = 3;
game.updateCapturedLocalVariable(anotherJ);
Las reglas siguientes se aplican al ámbito de las variables en las expresiones lambda:
Una variable capturada no se recolectará como elemento no utilizado hasta que el delegado que hace
referencia a ella sea elegible para la recolección de elementos no utilizados.
Las variables introducidas en una expresión lambda no son visibles en el método envolvente.
Una expresión lambda no puede capturar directamente un parámetro in, ref ni out desde el método
envolvente.
Una instrucción return en una expresión lambda no hace que el método envolvente devuelva un valor.
Una expresión lambda no puede contener una instrucción goto, break ni continue si el destino de esa
instrucción de salto está fuera del bloque de la expresión lambda. También es un error utilizar una
instrucción de salto fuera del bloque de la expresión lambda si el destino está dentro del bloque.
A partir de C# 9.0, puede aplicar el modificador static a una expresión lambda para evitar la captura
involuntaria de variables locales o el estado de la instancia por parte de la expresión lambda:
Una expresión lambda estática no puede capturar variables locales o el estado de la instancia desde ámbitos de
inclusión, pero puede hacer referencia a miembros estáticos y definiciones de constantes.
Vea también
Referencia de C#
Operadores y expresiones de C#
LINQ (Language Integrated Query)
Árboles de expresión
Funciones locales frente a expresiones lambda
Ejemplos de C# de Visual Studio 2008 (vea los archivos de ejemplos de consultas LINQ y el programa
XQuery)
Patrones (referencia de C#)
16/09/2021 • 15 minutes to read
C# presentó la coincidencia de patrones en C# 7.0. Desde entonces, cada versión principal de C# extiende las
capacidades de coincidencia de patrones. Las siguientes expresiones e instrucciones de C# admiten la
coincidencia de patrones:
Expresión is
switch Instrucción
switch Expresión (introducida en C# 8.0)
En esas construcciones, puede hacer coincidir una expresión de entrada con cualquiera de los siguientes
patrones:
Patrón de declaración: para comprobar el tipo en tiempo de ejecución de una expresión y, si una coincidencia
se realiza correctamente, asignar el resultado de una expresión a una variable declarada. Introducido en
C# 7.0.
Patrón de tipo: para comprobar el tipo en tiempo de ejecución de una expresión. Introducido en C# 9.0.
Patrón de constante: para probar si el resultado de una expresión es igual a una constante especificada.
Introducido en C# 7.0.
Patrones relacionales: para comparar el resultado de una expresión con una constante especificada.
Introducido en C# 9.0.
Patrones lógicos: para probar si una expresión coincide con una combinación lógica de patrones. Introducido
en C# 9.0.
Patrón de propiedad: para probar si las propiedades o los campos de una expresión coinciden con los
patrones anidados. Introducido en C# 8.0.
Patrón posicional: para deconstruir el resultado de una expresión y probar si los valores resultantes coinciden
con los patrones anidados. Introducido en C# 8.0.
Patrón var : para buscar coincidencias con cualquier expresión y asignar su resultado a una variable
declarada. Introducido en C# 7.0.
Patrón de descarte: para buscar coincidencias con cualquier expresión. Introducido en C# 8.0.
Los patrones lógicos, de propiedad y posicionales son patrones recursivos. Es decir, pueden contener patrones
anidados.
Para obtener el ejemplo de cómo usar esos patrones para compilar un algoritmo basado en datos, vea Tutorial:
Uso de la coincidencia de patrones para compilar algoritmos basados en tipos y basados en datos.
A partir de C# 7.0, un patrón de declaración con el tipo T coincide con una expresión cuando el resultado de
una expresión no es NULL y se cumple cualquiera de las condiciones siguientes:
El tipo en tiempo de ejecución del resultado de una expresión es T .
El tipo en tiempo de ejecución del resultado de una expresión deriva del tipo T , implementa una interfaz
T , o bien otra conversión de referencia implícita existe en T . En el ejemplo siguiente se muestran dos
casos en los que esta condición es verdadera:
En el ejemplo anterior, en la primera llamada al método GetSourceLabel , el primer patrón coincide con un
valor de argumento porque el tipo en tiempo de ejecución int[] del argumento deriva del tipo Array. En
la segunda llamada al método GetSourceLabel , el tipo en tiempo de ejecución List<T> del argumento no
deriva del tipo Array, pero implementa la interfaz ICollection<T>.
El tipo en tiempo de ejecución del resultado de una expresión es un tipo de valor que admite valores
NULL con el tipo subyacente T .
Existe una conversión boxing o unboxing del tipo en tiempo de ejecución del resultado de una expresión
al tipo T .
En el ejemplo siguiente se muestran las dos últimas condiciones:
int? xNullable = 7;
int y = 23;
object yBoxed = y;
if (xNullable is int a && yBoxed is int b)
{
Console.WriteLine(a + b); // output: 30
}
Si solo desea comprobar el tipo de una expresión, puede usar un patrón de descarte _ en lugar del nombre de
una variable, como se muestra en el ejemplo siguiente:
public abstract class Vehicle {}
public class Car : Vehicle {}
public class Truck : Vehicle {}
A partir de C# 9.0, para ese propósito se puede usar un patrón de tipo, como se muestra en el ejemplo siguiente:
Al igual que un patrón de declaración, un patrón de tipo coincide con una expresión cuando el resultado de una
expresión no es NULL y su tipo en tiempo de ejecución cumple cualquiera de las condiciones mencionadas
anteriormente.
Para obtener más información, vea las secciones Patrón de declaración y Patrón de tipo de las notas de
propuesta de características.
Patrón de constante
A partir de C# 7.0, se usa un patrón de constante para probar si el resultado de una expresión es igual a una
constante especificada, como se muestra en el ejemplo siguiente:
Use un patrón de constante para comprobar null , como se muestra en el ejemplo siguiente:
if (input is null)
{
return;
}
El compilador garantiza que no se invoca ningún operador de igualdad sobrecargado por el usuario == cuando
se evalúa la expresión x is null .
A partir de C# 9.0, se puede usar un patrón de constante negado null para comprobar si no es NULL, como se
muestra en el ejemplo siguiente:
Para obtener más información, vea la sección Patrón de constante de la nota de propuesta de características.
Patrones relacionales
A partir de C# 9.0, se usa un patrón relacional para comparar el resultado de una expresión con una constante,
como se muestra en el ejemplo siguiente:
En un patrón relacional, se puede usar cualquiera de los operadores relacionales < , > , <= o >= . La parte
derecha de un patrón relacional debe ser una expresión constante. La expresión constante puede ser de tipo
entero, de punto flotante, de carácter o de enumeración.
Para comprobar si el resultado de una expresión está en un intervalo determinado, busque coincidencias con un
patrón conjuntivo and , como se muestra en el ejemplo siguiente:
Si el resultado de una expresión es null o no se puede convertir al tipo de una constante mediante una
conversión que acepta valores NULL o unboxing, un patrón relacional no coincide con una expresión.
Para obtener más información, vea la sección Patrones relacionales de la nota de propuesta de características.
Patrones lógicos
A partir de C# 9.0, se usan los combinadores de patrones not , and y or para crear los siguientes patrones
lógicos:
Patrón de negación not que coincide con una expresión cuando el patrón negado no coincide con ella.
En el ejemplo siguiente se muestra cómo se puede negar un patrón de constante null para comprobar
si una expresión no es NULL:
Patrón conjuntivo and que coincide con una expresión cuando ambos patrones coinciden con ella. En el
ejemplo siguiente se muestra cómo se pueden combinar patrones relacionales para comprobar si un
valor se encuentra en un intervalo determinado:
Patrón disyuntivo or que coincide con una expresión cuando uno de los patrones coincide con ella,
como se muestra en el ejemplo siguiente:
Como se muestra en el ejemplo anterior, se puede usar repetidamente los combinadores de patrones en un
patrón.
El combinador de patrones and tiene mayor prioridad que or . Para especificar explícitamente la prioridad, use
paréntesis, como se muestra en el ejemplo siguiente:
static bool IsLetter(char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');
NOTE
El orden en el que se comprueban los patrones es indefinido. En tiempo de ejecución, se pueden comprobar primero los
patrones anidados del lado derecho de los patrones or y and .
Para obtener más información, vea la sección Combinadores de patrones de la nota de propuesta de
características.
Patrón de propiedad
A partir de C# 8.0, se usa un patrón de propiedad para que coincidan las propiedades o los campos de una
expresión con los patrones anidados, como se muestra en el ejemplo siguiente:
static bool IsConferenceDay(DateTime date) => date is { Year: 2020, Month: 5, Day: 19 or 20 or 21 };
Un patrón de propiedad coincide con una expresión cuando el resultado de una expresión no es NULL y cada
patrón anidado coincide con la propiedad o el campo correspondiente del resultado de la expresión.
También puede agregar una comprobación de tipo en tiempo de ejecución y una declaración de variable a un
patrón de propiedad, como se muestra en el ejemplo siguiente:
Un patrón de propiedad es un patrón recursivo. Es decir, se puede usar cualquier patrón como patrón anidado.
Use un patrón de propiedad para hacer coincidir partes de los datos con patrones anidados, como se muestra
en el ejemplo siguiente:
En el ejemplo anterior se usan dos características disponibles en C# 9.0 y versiones posteriores: or combinador
de patrones y tipos de registro.
A partir de C# 10.0, puede hacer referencia a propiedades o campos anidados dentro de un patrón de
propiedad. Por ejemplo, puede refactorizar el método del ejemplo anterior en el código equivalente siguiente:
static bool IsAnyEndOnXAxis(Segment segment) =>
segment is { Start.Y: 0 } or { End.Y: 0 };
Para obtener más información, consulte la sección Patrón de propiedad de la nota de propuesta de
características y la nota de propuesta de características Patrones de propiedades extendidos.
Patrón posicional
A partir de C# 8.0, se usa un patrón posicional para deconstruir el resultado de una expresión y hacer coincidir
los valores resultantes con los patrones anidados correspondientes, como se muestra en el ejemplo siguiente:
public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
}
En el ejemplo anterior, el tipo de una expresión contiene el método Deconstruct, que se usa para deconstruir el
resultado de una expresión. También puede hacer coincidir expresiones de tipos de tupla con patrones
posicionales. De este modo, puede hacer coincidir varias entradas con distintos patrones, como se muestra en el
ejemplo siguiente:
En el ejemplo anterior se usan patrones relacionales y lógicos, que están disponibles en C# 9.0 y versiones
posteriores.
Puede usar los nombres de elementos de tupla y parámetros Deconstruct en un patrón posicional, como se
muestra en el ejemplo siguiente:
var numbers = new List<int> { 1, 2, 3 };
if (SumAndCount(numbers) is (Sum: var sum, Count: > 0))
{
Console.WriteLine($"Sum of [{string.Join(" ", numbers)}] is {sum}"); // output: Sum of [1 2 3] is 6
}
Use un patrón de propiedad dentro de un patrón posicional, como se muestra en el ejemplo siguiente:
static bool IsInDomain(WeightedPoint point) => point is (>= 0, >= 0) { Weight: >= 0.0 };
Un patrón posicional es un patrón recursivo. Es decir, se puede usar cualquier patrón como patrón anidado.
Para obtener más información, vea la sección Patrón posicional de la nota de propuesta de características.
Patrón var
A partir de C# 7.0, se usa un patrón var para buscar coincidencias con cualquier expresión, incluida null , y se
asigna el resultado a una nueva variable local, como se muestra en el ejemplo siguiente:
Un patrón var es útil cuando se necesita una variable temporal dentro de una expresión booleana para
contener el resultado de los cálculos intermedios. También se puede usar un patrón var cuando necesite
realizar comprobaciones adicionales en las restricciones de caso when de una expresión o instrucción switch ,
como se muestra en el ejemplo siguiente:
En el ejemplo anterior, el patrón var (x, y) es equivalente a un patrón posicional (var x, var y) .
En un patrón var , el tipo de una variable declarada es el tipo en tiempo de compilación de la expresión que
coincide con el patrón.
Para obtener más información, vea la sección Patrón Var de la nota de propuesta de características.
Patrón de descarte
A partir de C# 8.0, se usa un patrón de descarte _ para buscar coincidencias con cualquier expresión, incluida
null , como se muestra en el ejemplo siguiente:
Console.WriteLine(GetDiscountInPercent(DayOfWeek.Friday)); // output: 5.0
Console.WriteLine(GetDiscountInPercent(null)); // output: 0.0
Console.WriteLine(GetDiscountInPercent((DayOfWeek)10)); // output: 0.0
En el ejemplo anterior, se usa un patrón de descarte para controlar null y cualquier valor entero que no tenga
el miembro correspondiente de la enumeración DayOfWeek. Esto garantiza que una expresión switch en el
ejemplo controle todos los valores de entrada posibles. Si no se usa un patrón de descarte en una expresión
switch y ninguno de los patrones de la expresión coincide con una entrada, el tiempo de ejecución produce
una excepción. El compilador genera una advertencia si una expresión switch no controla todos los valores de
entrada posibles.
Un patrón de descarte no puede ser un patrón en una expresión is ni una instrucción switch . En esos casos,
para buscar coincidencias con cualquier expresión, use un patrón var con un patrón de descarte: var _ .
Para obtener más información, vea la sección Patrón de descarte de la nota de propuesta de características.
Vea también
Referencia de C#
Operadores y expresiones de C#
Tutorial: Uso de la coincidencia de patrones para compilar algoritmos basados en tipos y basados en datos
Operadores + y += (referencia de C#)
16/09/2021 • 2 minutes to read
Los operadores + y += son compatibles con los tipos numéricos enteros y de punto flotante, el tipo string y
los tipos delegados.
Para obtener información acerca del operador aritmético + , consulte las secciones correspondientes a los
operadores unarios más y menos y al operador de suma + del artículo Operadores aritméticos.
Concatenación de cadenas
Cuando uno o ambos operandos son de tipo string, el operador + concatena las representaciones de cadena
de sus operandos (la representación de cadena de null es una cadena vacía):
A partir de C# 6, la interpolación de cadenas proporciona una manera más conveniente de dar formato a las
cadenas:
A partir de C# 10, se puede utilizar la interpolación de cadenas para inicializar una cadena constante cuando
todas las expresiones utilizadas para los marcadores de posición son también cadenas constantes.
Combinación de delegados
Para los operandos del mismo tipo de delegado, el operador + devuelve una nueva instancia de delegado que,
cuando se invoca, invoca el operando de la izquierda y luego invoca el operando de la derecha. Si alguno de los
operandos es null , el operador + devuelve el valor del otro operando (que también podría ser null ). El
ejemplo siguiente muestra cómo los delegados se pueden combinar con el operador + :
es equivalente a
x = x + y
int i = 5;
i += 9;
Console.WriteLine(i);
// Output: 14
Console.WriteLine();
printer += () => Console.Write("b");
printer(); // output: ab
También usa el operador += para especificar un método de controlador de eventos cuando se suscribe a un
evento. Para obtener más información, vea Procedimientos para suscribir y cancelar la suscripción a eventos.
Vea también
Referencia de C#
Operadores y expresiones de C#
Concatenación de varias cadenas
Eventos
Operadores aritméticos
Operadores - y -=
Operadores - y -= (referencia de C#)
16/09/2021 • 3 minutes to read
Los operadores - y -= son compatibles con los tipos numéricos enteros y de punto flotante, y los tipos
delegados.
Para obtener información sobre el operador aritmético - , consulte las secciones correspondientes a los
operadores unarios más y menos y al operador de resta (-) del artículo Operadores aritméticos.
Eliminación de delegados
Para los operandos del mismo tipo delegado, el operador - devuelve una instancia de delegado que se calcula
de la siguiente manera:
Si ambos operandos no son nulos y la lista de invocación del operando derecho es una sublista
apropiada contigua de la lista de invocación del operando izquierdo, el resultado de la operación es una
nueva lista de invocación que se obtiene mediante la eliminación de las entradas del operando derecho
de la lista de invocación del operando izquierdo. Si la lista del operando derecho coincide con varias
sublistas contiguas en la lista del operando izquierdo, se quita solo la sublista coincidente más a la
derecha. Si la eliminación da como resultado una lista vacía, el resultado es null .
var abbaab = a + b + b + a + a + b;
abbaab(); // output: abbaab
Console.WriteLine();
var ab = a + b;
var abba = abbaab - ab;
abba(); // output: abba
Console.WriteLine();
Si la lista de invocación del operando derecho no es una sublista apropiada contigua de la lista de
invocación del operando izquierdo, el resultado de la operación es el operando izquierdo. Por ejemplo, la
eliminación de un delegado que no forma parte del delegado de multidifusión no surte ningún efecto y
da como resultado que el delegado de multidifusión no cambie.
Action a = () => Console.Write("a");
Action b = () => Console.Write("b");
var abbaab = a + b + b + a + a + b;
var aba = a + b + a;
El ejemplo anterior también demuestra que, durante la eliminación de delegados, se comparan las
instancias de delegados. Por ejemplo, los delegados que se producen de la evaluación de expresiones
lambda idénticas no son iguales. Para obtener más información acerca de la igualdad de delegados,
consulte la sección Operadores de igualdad de delegado de la especificación del lenguaje C#.
Si el operando izquierdo es null , el resultado de la operación es null . Si el operando derecho es null ,
el resultado de la operación es el operando izquierdo.
x -= y
es equivalente a
x = x - y
Console.WriteLine();
printer -= a;
printer(); // output: ab
También se usa el operador -= con el fin de especificar un método de controlador de eventos para eliminar
cuando se finaliza la suscripción a un evento. Para obtener más información, vea Procedimiento para suscribir y
cancelar la suscripción a eventos.
Vea también
Referencia de C#
Operadores y expresiones de C#
Eventos
Operadores aritméticos
Operadores + y +=
Operador ?: (referencia de C#)
16/09/2021 • 3 minutes to read
El operador condicional ?: , también conocido como operador condicional ternario, evalúa una expresión
booleana y devuelve el resultado de una de las dos expresiones, en función de que la expresión booleana se
evalúe como true o false , tal y como se muestra en el siguiente ejemplo:
La expresión condition debe evaluarse como true o false . Si condition se evalúa como true , se evalúa la
expresión consequent y su resultado se convierte en el resultado de la operación. Si condition se evalúa como
false , se evalúa la expresión alternative y su resultado se convierte en el resultado de la operación. Solo se
evalúan consequent o alternative .
A partir de C# 9.0, las expresiones condicionales tienen tipo de destino. Es decir, si se conoce el tipo de destino
de una expresión condicional, los tipos de consequent y alternative se deben poder convertir implícitamente
al tipo de destino, como se muestra en el ejemplo siguiente:
Si se desconoce el tipo de destino de una expresión condicional (por ejemplo, al usar la palabra clave var ) o en
C# 8.0 y versiones anteriores, el tipo de consequent y alternative debe ser el mismo, o bien debe haber una
conversión implícita de un tipo a otro:
a ? b : c ? d : e
se evalúa como
a ? b : (c ? d : e)
TIP
Puede utilizar el siguiente recurso mnemotécnico para recordar cómo se evalúa el operador condicional:
Como sucede con el operador condicional original, una expresión condicional ref evalúa solo una de las dos
expresiones, ya sea consequent o alternative .
En el caso de una expresión condicional ref, los tipos de consequent y alternative deben coincidir. Las
expresiones condicionales ref no tienen tipo de destino.
En el ejemplo siguiente se muestra el uso de una expresión condicional ref:
int index = 7;
ref int refValue = ref ((index < 5) ? ref smallArray[index] : ref largeArray[index - 5]);
refValue = 0;
index = 2;
((index < 5) ? ref smallArray[index] : ref largeArray[index - 5]) = 100;
string classify;
if (input >= 0)
{
classify = "nonnegative";
}
else
{
classify = "negative";
}
Vea también
Referencia de C#
Operadores y expresiones de C#
if (Instrucción)
Operadores ?. y ?[]
Operadores ?? y ??=
ref (palabra clave)
! (permite valores NULL) (referencia de C#)
16/09/2021 • 2 minutes to read
Disponible en C# 8.0 y versiones posteriores, el operador ! de postfijo unario es el operador que permite
valores NULL o supresión de valores NULL. En un contexto de anotación que admite un valor NULL habilitado,
se usa el operador que permite un valor NULL para declarar que la expresión x de un tipo de referencia no es
null : x! . El operador ! de prefijo unario es el operador lógico de negación.
El operador que permite un valor NULL no tiene ningún efecto en tiempo de ejecución. Solo afecta al análisis de
flujo estático del compilador al cambiar el estado NULL de la expresión. En tiempo de ejecución, la expresión
x! se evalúa en el resultado de la expresión subyacente x .
Para obtener más información sobre la característica de tipos de referencia que admiten un valor NULL, consulte
Tipos de referencia que admiten un valor NULL.
Ejemplos
Uno de los casos de uso del operador que permite un valor NULL es probar la lógica de validación de
argumentos. Por ejemplo, considere la siguiente clase:
#nullable enable
public class Person
{
public Person(string name) => Name = name ?? throw new ArgumentNullException(nameof(name));
Con el marco de pruebas MSTest puede crear la prueba siguiente para la lógica de validación en el constructor:
[TestMethod, ExpectedException(typeof(ArgumentNullException))]
public void NullNameShouldThrowTest()
{
var person = new Person(null!);
}
Sin el operador que permite un valor NULL, el compilador genera la advertencia siguiente para el código
anterior: Warning CS8625: Cannot convert null literal to non-nullable reference type . Al usar el operador que
permite un valor NULL, se notifica al compilador que lo esperado es que se pase null y no se debe advertir al
respecto.
También puede usar el operador que permite valores NULL cuando sepa a ciencia cierta que una expresión no
puede ser null , pero el compilador no consigue reconocerlo. En el ejemplo siguiente, si el método IsValid
devuelve true , su argumento no es null y puede desreferenciarlo de forma segura:
public static void Main()
{
Person? p = Find("John");
if (IsValid(p))
{
Console.WriteLine($"Found {p!.Name}");
}
}
Sin el operador que permite un valor NULL, el compilador genera la advertencia siguiente para el código
p.Name : Warning CS8602: Dereference of a possibly null reference .
Si puede modificar el método IsValid , puede usar el atributo NotNullWhen para notificar al compilador que un
argumento del método IsValid no puede ser null cuando el método devuelve true :
En el ejemplo anterior, no es necesario usar el operador que permite un valor NULL porque el compilador tiene
suficiente información para averiguar que p no puede ser null en la instrucción if . Para más información
sobre los atributos que permiten proporcionar información adicional sobre el estado NULL de una variable, vea
Actualización de las API con atributos para definir las expectativas NULL.
Vea también
Referencia de C#
Operadores y expresiones de C#
Tutorial: Diseño con tipos de referencia que admiten un valor NULL
?? Operadores ?? y ?? (referencia de C#)
16/09/2021 • 2 minutes to read
El operador de uso combinado de NULL ?? devuelve el valor del operando izquierdo si no es null ; en caso
contrario, evalúa el operando derecho y devuelve su resultado. El operador ?? no evalúa su operando derecho
si el operando izquierdo se evalúa como no NULL.
Disponible en C# 8.0 y versiones posteriores, el operador de asignación de uso combinado de NULL ??= asigna
el valor de su operando derecho al operando izquierdo solo si el operando izquierdo se evalúa como null . El
operador ??= no evalúa su operando derecho si el operando izquierdo se evalúa como no NULL.
El operando izquierdo del operador ??= debe ser una variable, una propiedad o un elemento de indizador.
En C# 7.3 y versiones anteriores, el tipo del operando izquierdo del operador ?? debe ser un tipo de referencia
o un tipo de valor que acepta valores NULL. A partir C# 8.0, ese requisito se reemplaza por lo siguiente: el tipo
del operando izquierdo de los operadores ?? y ??= no puede ser un tipo de valor que no acepta valores
NULL. En concreto, a partir de C# 8.0 puede usar los operadores de fusión de NULL con parámetros de tipo sin
restricciones:
Los operadores de uso combinado de NULL son asociativos a la derecha. Es decir, las expresiones del formulario
a ?? b ?? c
d ??= e ??= f
se evalúan como
a ?? (b ?? c)
d ??= (e ??= f)
Ejemplos
Los operadores ?? y ??= puede resultar útiles en los siguientes escenarios:
En expresiones con los operadores no condicionales ? y ?[], puede usar el operador ?? para
proporcionar una expresión alternativa para evaluar en caso de que el resultado de la expresión con la
operación condicional NULL sea null :
Cuando trabaja con tipos de valor que aceptan valores NULL y necesita proporcionar un valor de un tipo
de valor subyacente, use el operador ?? para especificar el valor para proporcionar en caso de que un
valor de tipo que acepta valores NULL sea null :
int? a = null;
int b = a ?? -1;
Console.WriteLine(b); // output: -1
Use el método Nullable<T>.GetValueOrDefault() si el valor que se va usar cuando un valor de tipo que
acepta valores NULL es null debe ser el valor predeterminado del tipo de valor subyacente.
A partir de C# 7.0, puede usar una expresión throw como el operando derecho del operador ?? para
hacer el código de comprobación de argumentos más conciso:
El ejemplo anterior también muestra cómo usar miembros con forma de expresión para definir una
propiedad.
A partir de C# 8.0, puede usar el operador ??= para reemplazar el código del formulario
if (variable is null)
{
variable = expression;
}
El token => se admite de dos formas: como el operador lambda y como un separador de un nombre de
miembro y la implementación del miembro en una definición de cuerpo de expresión.
Operador{1}{2}lambda
En las expresiones lambda, el operador => {4}lambda {5} separa los parámetros de entrada del lado izquierdo y
el cuerpo lambda del lado derecho.
En el ejemplo siguiente se usa la característica LINQ con sintaxis de método para demostrar el uso de las
expresiones lambda:
int[] numbers = { 4, 7, 10 };
int product = numbers.Aggregate(1, (interim, next) => interim * next);
Console.WriteLine(product); // output: 280
Los parámetros de entrada de una expresión lambda están fuertemente tipados en tiempo de compilación.
Cuando el compilador puede deducir los tipos de los parámetros de entrada, como en el ejemplo anterior, se
pueden omitir las declaraciones de tipos. Si tiene que especificar el tipo de los parámetros de entrada, debe
hacerlo para cada uno de ellos, como se muestra en el ejemplo siguiente:
int[] numbers = { 4, 7, 10 };
int product = numbers.Aggregate(1, (int interim, int next) => interim * next);
Console.WriteLine(product); // output: 280
En el ejemplo siguiente se muestra cómo definir una expresión lambda sin parámetros de entrada:
donde expression es una expresión válida. El tipo de valor devuelto de expression se debe poder convertir
implícitamente al tipo de valor devuelto del miembro. Si el tipo de valor devuelto del miembro es void , o si el
miembro es un constructor, un finalizador o un descriptor de acceso set de propiedad o indizador, expression
debe ser una expresión de instrucción. Dado que el resultado de la expresión se descarta, el tipo de valor
devuelto de esa expresión puede ser cualquiera.
En el ejemplo siguiente se muestra una definición de cuerpo de expresión para un método Person.ToString :
Vea también
Referencia de C#
Operadores y expresiones de C#
Operador :: (referencia de C#)
16/09/2021 • 2 minutes to read
Use el calificador de alias de espacio de nombres :: para acceder a un miembro del espacio de nombres con
alias. Solo puede usar el calificador :: entre dos identificadores. El identificador de la izquierda puede ser
cualquiera de los siguientes alias:
Un alias de espacio de nombres creado con una directiva de alias using:
Un alias externo.
El alias global , que es el alias del espacio de nombres global. El espacio de nombres global es el espacio
de nombres que contiene los espacios de nombres y los tipos que no se declaran dentro de un espacio de
nombres con nombre. Cuando se usa con el calificador :: , el alias global siempre hace referencia al
espacio de nombres global, incluso si está el alias del espacio de nombres global definido por el
usuario.
En el ejemplo siguiente se usa el alias global para tener acceso al espacio de nombres System de .NET,
que es miembro del espacio de nombres global. Sin el alias global , se tendría acceso al espacio de
nombres System definido por el usuario, que es miembro del espacio de nombres MyCompany.MyProduct :
namespace MyCompany.MyProduct.System
{
class Program
{
static void Main() => global::System.Console.WriteLine("Using global alias");
}
class Console
{
string Suggestion => "Consider renaming this class";
}
}
NOTE
La palabra clave global es el alias de espacio de nombres global solo cuando es el identificador izquierdo del
calificador :: .
También puede usar el token . para acceder a un miembro de un espacio de nombres con alias. Sin embargo,
el operador . también se usa para acceder a un miembro del tipo. El calificador :: garantiza que el
identificador de la izquierda siempre hace referencia a un alias de espacio de nombres, aunque exista un tipo o
un espacio de nombres con el mismo nombre.
Especificación del lenguaje C#
Para más información, consulte la sección sobre calificadores de alias de espacio de nombres de la
Especificación del lenguaje C#.
Vea también
Referencia de C#
Operadores y expresiones de C#
Uso de espacios de nombres
Operador await (referencia de C#)
16/09/2021 • 3 minutes to read
El operador await suspende la evaluación del método async envolvente hasta que se completa la operación
asincrónica representada por su operando. Cuando se completa la operación asincrónica, el operador await
devuelve el resultado de la operación, si existe. Cuando el operador await se aplica al operando que representa
una operación ya completada, devuelve el resultado de la operación inmediatamente sin suspender el método
envolvente. El operador await no bloquea el subproceso que evalúa el método async. Cuando el operador
await suspende el método async envolvente, el control vuelve al autor de la llamada del método.
using System;
using System.Net.Http;
using System.Threading.Tasks;
En el ejemplo anterior se usa el método async Main , lo que es posible a partir de C# 7.1. Para obtener más
información, vea la sección Operador await en el método Main.
NOTE
Para obtener una introducción a la programación asincrónica, vea Programación asincrónica con async y await. La
programación asincrónica con async y await sigue el modelo asincrónico basado en tareas.
El operador await se puede usar solamente en un método, una expresión lambda o un método anónimo
modificado por la palabra clave async. Dentro de un método async, no se puede usar el operador await en el
cuerpo de una función sincrónica, dentro del bloque de una instrucción lock y en un contexto no seguro.
El operando del operador await suele ser de uno de los siguientes tipos de .NET: Task, Task<TResult>, ValueTask
o ValueTask<TResult>. Aunque cualquier expresión con await puede ser el operando del operador await . Para
obtener más información, vea la sección Expresiones con await de la especificación del lenguaje C#.
El tipo de expresión await t es TResult si el tipo de expresión t es Task<TResult> o ValueTask<TResult>. Si
el tipo de t es Task o ValueTask, el tipo de await t es void . En ambos casos, si t produce una excepción,
await t vuelve a producir la excepción. Para obtener más información sobre cómo controlar las excepciones,
vea la sección Excepciones en métodos async del artículo Instrucción try-catch.
Las palabras clave async y await están disponibles en C# 5 y versiones posteriores.
Vea también
Referencia de C#
Operadores y expresiones de C#
async
Modelo de programación asincrónica de tareas
Programación asincrónica
Async en profundidad
Tutorial: Acceso a la web con async y await
Tutorial: Generación y uso de secuencias asincrónicas con C# 8.0 y .NET Core 3.0
Expresiones de valor predeterminado (referencia de
C#)
16/09/2021 • 2 minutes to read
Una expresión de valor predeterminado genera el valor predeterminado de un tipo. Hay dos tipos de
expresiones de valor predeterminado: la llamada al operador predeterminado y un literal predeterminado.
También se usa la palabra clave default como etiqueta de mayúsculas y minúsculas predeterminada dentro de
una instrucción switch .
operador default
El argumento del operador default debe ser el nombre de un tipo o un parámetro de tipo, como se muestra en
el ejemplo siguiente:
Console.WriteLine(default(int)); // output: 0
Console.WriteLine(default(object) is null); // output: True
void DisplayDefaultOf<T>()
{
var val = default(T);
Console.WriteLine($"Default value of {typeof(T)} is {(val == null ? "null" : val.ToString())}.");
}
DisplayDefaultOf<int?>();
DisplayDefaultOf<System.Numerics.Complex>();
DisplayDefaultOf<System.Collections.Generic.List<int>>();
// Output:
// Default value of System.Nullable`1[System.Int32] is null.
// Default value of System.Numerics.Complex is (0, 0).
// Default value of System.Collections.Generic.List`1[System.Int32] is null.
Literal default
A partir C# de 7.1, puede usar el literal default para generar el valor predeterminado de un tipo cuando el
compilador puede deducir el tipo de expresión. La expresión literal default genera el mismo valor que la
expresión default(T) cuando se deduce el tipo T . Puede usar el literal default en cualquiera de los casos
siguientes:
En la asignación o inicialización de una variable.
En la declaración del valor predeterminado de un parámetro de método opcional.
En una llamada al método para proporcionar un valor de argumento.
En una instrucción return o como una expresión de un miembro con cuerpo de expresión.
En el ejemplo siguiente se muestra el uso del literal default :
T[] InitializeArray<T>(int length, T initialValue = default)
{
if (length < 0)
{
throw new ArgumentOutOfRangeException(nameof(length), "Array length must be nonnegative.");
}
Display(InitializeArray<int>(3)); // output: [ 0, 0, 0 ]
Display(InitializeArray<bool>(4, default)); // output: [ False, False, False, False ]
Vea también
Referencia de C#
Operadores y expresiones de C#
Valores predeterminados de los tipos de C#
Elementos genéricos en .NET
Operador delegate (referencia de C#)
16/09/2021 • 2 minutes to read
El operador delegate crea un método anónimo que se puede convertir en un tipo delegado:
NOTE
A partir de C# 3, las expresiones lambda proporcionan una manera más concisa y expresiva de crear una función
anónima. Use el operador => para construir una expresión lambda:
Para más información sobre las características de las expresiones lambda, como capturar las variables externas, consulte
Expresiones lambda.
Al usar el operador delegate , puede omitir la lista de parámetros. Si lo hace, el método anónimo creado se
puede convertir en un tipo delegado con cualquier lista de parámetros, tal como se muestra en el ejemplo
siguiente:
// Output:
// Hello!
// This is world!
Esta es la única funcionalidad de los métodos anónimos que las expresiones lambda no admiten. En todos los
demás casos, una expresión lambda es una de las maneras preferidas de escribir código alineado.
A partir de C# 9.0, puede usar descartes para especificar dos o más parámetros de entrada de un método
anónimo que no usa el método:
Por compatibilidad con versiones anteriores, si solo un parámetro se denomina _ , _ se trata como el nombre
de ese parámetro dentro de un método anónimo.
También a partir de C# 9.0, puede usar el modificador static en la declaración de un método anónimo:
Vea también
Referencia de C#
Operadores y expresiones de C#
Operador =>
Operador is (Referencia de C#)
16/09/2021 • 2 minutes to read
El operador is comprueba si el resultado de una expresión es compatible con un tipo determinado. Para
obtener información sobre el operador de prueba de tipos is , vea la sección operador is del artículo
Operadores de conversión y prueba de tipos.
A partir de C# 7.0, también puede usar el operador is para comparar una expresión con un patrón, como se
muestra en el ejemplo siguiente:
static bool IsFirstSummerMonday(DateTime date) => date is { Month: 6, Day: <=7, DayOfWeek: DayOfWeek.Monday
};
En el ejemplo anterior, el operador is compara una expresión con un patrón de propiedad con patrones
constantes y relacionales anidados.
El operador is puede resultar útil en los siguientes escenarios:
Para comprobar el tipo en tiempo de ejecución de una expresión, como se muestra en el ejemplo
siguiente:
int i = 34;
object iBoxed = i;
int? jNullable = 42;
if (iBoxed is int a && jNullable is int b)
{
Console.WriteLine(a + b); // output 76
}
if (input is null)
{
return;
}
Al comparar una expresión con null , el compilador garantiza que no se invoca ningún operador == o
!= sobrecargado por el usuario.
A partir de C# 9.0, puede usar un patrón de negación para realizar una comprobación de valores distintos
de NULL, como se muestra en el ejemplo siguiente:
Vea también
Referencia de C#
Operadores y expresiones de C#
Patrones
Tutorial: Uso de la coincidencia de patrones para compilar algoritmos basados en tipos y basados en datos
Operadores de conversión y prueba de tipos
Expresión nameof (referencia de C#)
16/09/2021 • 2 minutes to read
Una expresión nameof genera el nombre de una variable, un tipo o un miembro como constante de cadena:
Como se muestra en el ejemplo anterior, en el caso de un tipo y un espacio de nombres, el nombre generado no
está completo.
En el caso de identificadores textuales, el carácter @ no es parte de un nombre, como se muestra en el ejemplo
siguiente:
var @new = 5;
Console.WriteLine(nameof(@new)); // output: new
Vea también
Referencia de C#
Operadores y expresiones de C#
Operador new (referencia de C#)
16/09/2021 • 3 minutes to read
Puede usar un inicializador de colección u objeto con el operador new para crear una instancia e inicializar un
objeto en una sola instrucción, como se muestra en el ejemplo siguiente:
A partir de C# 9.0, las expresiones de invocación del constructor tienen tipo de destino. Es decir, si se conoce el
tipo de destino de una expresión, puede omitir un nombre de tipo, como se muestra en el ejemplo siguiente:
List<int> xs = new();
List<int> ys = new(capacity: 10_000);
List<int> zs = new() { Capacity = 20_000 };
Como se muestra en el ejemplo anterior, siempre se usan paréntesis en una expresión new con tipo de destino.
Si se desconoce el tipo de destino de una expresión new (por ejemplo, cuando se usa la palabra clave var ), se
debe especificar un nombre de tipo.
creación de matriz
También se usa el operador new para crear una instancia de matriz, como se muestra en el ejemplo siguiente:
Utilice la sintaxis de inicialización de matriz para crear una instancia de matriz y rellenarla con los elementos en
una sola instrucción. En el ejemplo siguiente se muestran varias maneras de hacerlo:
El operador sizeof devuelve el número de bytes ocupados por una variable de un tipo determinado. El
argumento del operador sizeof debe ser el nombre de un tipo administrado o un parámetro de tipo que está
restringido para ser un tipo no administrado.
El operador sizeof requiere un contexto de unsafe. Sin embargo, las expresiones presentadas en la tabla
siguiente se evalúan en tiempo de compilación según los valores de constantes correspondientes y no necesitan
un contexto de unsafe:
sizeof(sbyte) 1
sizeof(byte) 1
sizeof(short) 2
sizeof(ushort) 2
sizeof(int) 4
sizeof(uint) 4
sizeof(long) 8
sizeof(ulong) 8
sizeof(char) 2
sizeof(float) 4
sizeof(double) 8
sizeof(decimal) 16
sizeof(bool) 1
Tampoco es necesario usar un contexto de unsafe cuando el operando del operador sizeof es el nombre de un
tipo enum.
En el siguiente ejemplo se muestra el uso del operador sizeof :
using System;
unsafe
{
Console.WriteLine(sizeof(Point*)); // output: 8
}
}
El operador sizeof devuelve un número de bytes que asignará Common Language Runtime en la memoria
administrada. Para los tipos struct, el valor incluye el relleno, tal y como se muestra en el ejemplo anterior. El
resultado del operador sizeof puede ser distinto del resultado del método Marshal.SizeOf, que devuelve el
tamaño de un tipo en la memoria no administrada.
Vea también
Referencia de C#
Operadores y expresiones de C#
Operadores relacionados con el puntero
Tipos de puntero
Tipos relacionados con el intervalo y la memoria
Elementos genéricos en .NET
Expresión stackalloc (referencia de C#)
16/09/2021 • 3 minutes to read
La expresión stackalloc asigna un bloque de memoria en la pila. Un bloque de memoria asignado a la pila
creado durante la ejecución del método se descarta automáticamente cuando se devuelva dicho método. No
puede liberar explícitamente memoria asignada con stackalloc . Un bloque de memoria asignada a la pila no
está sujeto a la recolección de elementos no utilizados y no tiene que fijarse con una instrucción fixed .
Puede asignar el resultado de una expresión stackalloc a una variable de uno de los siguientes tipos:
A partir de C# 7.2, System.Span<T> o System.ReadOnlySpan<T>, como se muestra en el ejemplo
siguiente:
int length = 3;
Span<int> numbers = stackalloc int[length];
for (var i = 0; i < length; i++)
{
numbers[i] = i;
}
No tiene que usar un contexto unsafe al asignar un bloque de memoria asignado a la pila para una
variable Span<T> o ReadOnlySpan<T>.
Cuando se trabaja con esos tipos, puede usar una expresión stackalloc en expresiones condicionales o
de asignación, como se muestra en el ejemplo siguiente:
A partir de C# 8.0, se puede usar una expresión stackalloc dentro de otras expresiones siempre que se
permita una variable Span<T> o ReadOnlySpan<T>, como se muestra en el ejemplo siguiente:
NOTE
Se recomienda usar los tipos Span<T> o ReadOnlySpan<T> para trabajar con memoria asignada a la pila siempre
que sea posible.
Como se muestra en el ejemplo anterior, se debe utilizar un contexto unsafe cuando se trabaja con tipos
de puntero.
En el caso de los tipos de puntero, solo se puede usar una expresión stackalloc en una declaración de
variable local para inicializar la variable.
La cantidad de memoria disponible en la pila es limitada. Si asigna demasiada memoria en la pila, se produce
una excepción StackOverflowException. Para evitarlo, siga estas reglas:
Limite la cantidad de memoria asignada con stackalloc . Por ejemplo, si el tamaño de búfer previsto está
por debajo de un límite determinado, asigne la memoria en la pila; de lo contrario, use una matriz de la
longitud necesaria, como se muestra en el código siguiente:
NOTE
Como la cantidad de memoria disponible en la pila depende del entorno en el que se ejecuta el código, debe ser
conservador al definir el valor límite real.
Evite el uso de stackalloc dentro de bucles. Asigne el bloque de memoria fuera de un bucle y vuelva a
usarlo dentro del bucle.
El contenido de la memoria recién asignada está sin definir. Debe inicializarlo antes del uso. Por ejemplo, puede
usar el método Span<T>.Clear que establece todos los elementos en el valor predeterminado de tipo T .
A partir de C# 7.3, puede usar la sintaxis de inicializador de matriz para definir el contenido de la memoria
recién asignada. En el ejemplo siguiente se muestran diversas formas de hacerlo:
En la expresión stackalloc T[E] , T debe ser un tipo no administrado y E debe evaluarse como un valor int
no negativo.
Seguridad
El uso de stackalloc habilita automáticamente las características de detección de saturación del búfer en el
entorno Common Language Runtime (CLR). Si se detecta saturación del búfer, se finaliza el proceso lo antes
posible para minimizar el riesgo de que se ejecute código malintencionado.
Especificación del lenguaje C#
Para más información, consulte la sección sobre asignación de pila de la especificación del lenguaje C# y la nota
de la característica Permitir stackalloc en contextos anidados.
Vea también
Referencia de C#
Operadores y expresiones de C#
Operadores relacionados con el puntero
Tipos de puntero
Tipos relacionados con el intervalo y la memoria
Qué hacer y qué no hacer de stackalloc
Expresión switch (referencia de C#)
16/09/2021 • 3 minutes to read
A partir de C# 8.0, se usa la expresión switch para evaluar una expresión única a partir de una lista de
expresiones candidatas basada en una coincidencia de patrón con una expresión de entrada. Para obtener
información sobre la instrucción switch que admite la semántica switch en un contexto de instrucción,
consulte la sección instrucción switch del artículo Instrucciones de selección.
En el ejemplo siguiente se muestra una expresión switch , que traslada los valores de un objeto enum que
representa las direcciones visuales de un mapa en línea hasta la dirección cardinal correspondiente:
IMPORTANT
Para obtener información sobre los patrones admitidos por una expresión switch y más ejemplos, consulte Patrones.
El resultado de la expresión switch es el valor de la expresión del primer brazo de la expresión switch cuyo
patrón coincide con la expresión de intervalo y cuya restricción de mayúsculas y minúsculas, en caso de estar
presente, se evalúa como true . Los brazos de la expresión switch se evalúan en orden de texto.
El compilador emite un error cuando no se puede elegir un brazo de la expresión switch inferior porque un
brazo de la expresión switch superior coincide con todos sus valores.
TIP
Para garantizar que una expresión switch controle todos los valores de entrada posibles, proporcione un brazo de
expresión switch con un patrón de descarte.
Especificación del lenguaje C#
Para obtener más información, vea la sección sobre la expresión de switch de la nota de propuesta de
características.
Vea también
Referencia de C#
Operadores y expresiones de C#
Patrones
Tutorial: Uso de la coincidencia de patrones para compilar algoritmos basados en tipos y basados en datos
Instrucción switch
Operadores true y false (referencia de C#)
16/09/2021 • 2 minutes to read
El operador true devuelve el valor bool true para indicar que su operando es definitivamente true. El
operador false devuelve el valor bool``true para indicar que su operando es definitivamente false. Los
operadores true y false no garantizan que se complementan entre sí. Es decir, tanto el operador true como
false podrían devolver el valor bool``false del mismo operando. Si un tipo define uno de los dos
operadores, también debe definir otro operador.
TIP
Use el tipo bool? , si tiene que admitir la lógica de tres valores (por ejemplo, cuando trabaja con bases de datos que
admiten un tipo booleano de tres valores). C# proporciona los operadores & y | que admiten la lógica de tres valores
con los operandos bool? . Para más información, consulte la sección Operadores lógicos booleanos que aceptan valores
NULL del artículo Operadores lógicos booleanos.
Expresiones booleanas
Un tipo con el operador true definido puede ser el tipo de un resultado de una expresión condicional de
control en las instruciones if, do, while y for y en el operador condicional ?: . Para más información, vea la
sección Expresiones booleanas de la Especificación del lenguaje C#.
Ejemplo
El ejemplo siguiente muestra el tipo que define los operadores true y false . Además, el tipo sobrecarga el
operador lógico AND & de manera que el operador && también se puede evaluar para los operandos de ese
tipo.
using System;
if (x == Yellow || y == Yellow)
{
return Yellow;
}
return Green;
}
public override bool Equals(object obj) => obj is LaunchStatus other && this == other;
public override int GetHashCode() => status;
}
Tenga en cuenta el comportamiento de cortocircuito del operador && . Cuando el método GetFuelLaunchStatus
devuelve LaunchStatus.Red , el operando derecho del operador && no se evalúa. Eso es porque
LaunchStatus.Red es definitivamente false. A continuación, el resultado de AND lógico no depende del valor del
operando derecho. El resultado del ejemplo es el siguiente:
Getting fuel launch status...
Wait!
Vea también
Referencia de C#
Operadores y expresiones de C#
Expresión with (referencia de C#)
16/09/2021 • 3 minutes to read
Disponible en C# 9.0 y versiones posteriores, se trata de una expresión with que genera una copia de su
operando record con las propiedades y los campos especificados modificados:
using System;
var p3 = p1 with
{
Name = "C",
Y = 4
};
Console.WriteLine($"{nameof(p3)}: {p3}"); // output: p3: NamedPoint { Name = C, X = 0, Y = 4 }
Como se muestra en el ejemplo anterior, se usa la sintaxis de inicializador de objeto para especificar qué
miembros se van a modificar y sus nuevos valores. En una expresión with , un operando izquierdo debe ser de
un tipo de registro.
El resultado de una expresión with tiene el mismo tipo de entorno de ejecución que el operando de la
expresión, como se muestra en el ejemplo siguiente:
using System;
}
}
En el caso de un miembro de tipo de referencia, cuando se copia un registro solo se copia la referencia a una
instancia. Tanto la copia como el registro original tienen acceso a la misma instancia de tipo de referencia. En el
ejemplo siguiente se muestra ese comportamiento:
using System;
using System.Collections.Generic;
original.Tags.Add("C");
Console.WriteLine($"Tags of {nameof(copy)}: {copy.PrintTags()}");
// output: Tags of copy: A, B, C
}
}
Cualquier tipo de registro tiene el constructor de copia. Es un constructor con un único parámetro del tipo de
registro contenedor. Copia el estado de su argumento en una nueva instancia de registro. Al evaluar una
expresión with , se llama al constructor de copia para crear instancias de una nueva instancia de registro en
función de un registro original. Después, la nueva instancia se actualiza según las modificaciones especificadas.
De forma predeterminada, el constructor de copia es implícito, es decir, lo genera el compilador. Si tiene que
personalizar la semántica de la copia de registros, declare explícitamente un constructor de copia con el
comportamiento deseado. En el ejemplo siguiente se actualiza el anterior con un constructor de copia explícito.
El nuevo comportamiento de copia consiste en copiar los elementos de lista en lugar de una referencia de lista
cuando se copia un registro:
using System;
using System.Collections.Generic;
original.Tags.Add("C");
Console.WriteLine($"Tags of {nameof(copy)}: {copy.PrintTags()}");
// output: Tags of copy: A, B
}
}
Vea también
Referencia de C#
Operadores y expresiones de C#
Registros
Sobrecarga de operadores (referencia de C#)
16/09/2021 • 4 minutes to read
Un tipo definido por el usuario puede sobrecargar un operador de C# predefinido. Es decir, un tipo puede
proporcionar la implementación personalizada de una operación cuando uno o los dos operandos son de ese
tipo. En la sección Operadores sobrecargables se muestra qué operadores de C# pueden sobrecargarse.
Use la palabra clave operator para declarar un operador. Una declaración de operador debe cumplir las reglas
siguientes:
Incluye los modificadores public y static .
Un operador unario tiene un parámetro de entrada. Un operador binario tiene dos parámetros de entrada. En
cada caso, al menos un parámetro debe ser de tipo T o T? donde T es el tipo que contiene la declaración
del operador.
En el ejemplo siguiente se muestra una estructura simplificada para representar un número racional. La
estructura sobrecarga algunos de los operadores aritméticos:
using System;
Puede ampliar el ejemplo anterior mediante la definición de una conversión implícita de int a Fraction . A
continuación, los operadores sobrecargados admiten argumentos de esos dos tipos. Es decir, sería posible
agregar un valor entero a una fracción y obtener como resultado una fracción.
También usa la palabra clave operator para definir una conversión de tipos personalizada. Para obtener más
información, vea Operadores de conversión definidos por el usuario.
Operadores sobrecargables
En la tabla siguiente se proporciona información sobre la posibilidad de sobrecarga de los operadores de C#:
+x, -x, !x, ~x, ++, --, true, false Estos operadores unarios se pueden sobrecargar.
+=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>= Los operadores de asignación compuestos no pueden
sobrecargarse explícitamente. Pero cuando se sobrecarga un
operador binario, el operador de asignación compuesto
correspondiente, si lo hay, también se sobrecarga de modo
implícito. Por ejemplo, += se evalúa con + , que se pueden
sobrecargar.
^x, x = y, x.y, x?.y , c ? t : f, x ?? y, x ??= y, x..y, x->y, =>, Estos operadores no se pueden sobrecargar.
f(x), as, await, checked, unchecked, default, delegate, is,
nameof, new, sizeof, stackalloc, switch, typeof, with
NOTE
Los operadores de comparación deben sobrecargarse en pares. Es decir, si se sobrecarga un operador de un par, el otro
operador debe sobrecargarse también. Se emparejan de la siguiente manera:
Operadores == y !=
Operadores < y >
Operadores <= y >=
En cualquier punto del cuerpo de una instrucción de iteración, se puede salir del bucle mediante la instrucción
break, o bien se puede ir a la siguiente iteración del bucle mediante la instrucción continue.
Instrucción for
La instrucción for ejecuta una instrucción o un bloque de instrucciones mientras una expresión booleana
especificada se evalúa como true . En el ejemplo siguiente se muestra la instrucción for , que ejecuta su
cuerpo mientras que un contador entero sea menor que tres:
int i = 0
La sección condición que determina si se debe ejecutar la siguiente iteración del bucle. Si se evalúa como
true o no está presente, se ejecuta la siguiente iteración; de lo contrario, se sale del bucle. La sección
condición debe ser una expresión booleana.
La sección condición del ejemplo anterior comprueba si un valor de contador es menor que tres:
i < 3
La sección iterador, que define lo que sucede después de cada iteración del cuerpo del bucle.
La sección iterador del ejemplo anterior incrementa el contador:
i++
int i;
int j = 3;
for (i = 0, Console.WriteLine($"Start: i={i}, j={j}"); i < j; i++, j--, Console.WriteLine($"Step: i={i}, j=
{j}"))
{
//...
}
// Output:
// Start: i=0, j=3
// Step: i=1, j=2
// Step: i=2, j=1
Todas las secciones de la instrucción for son opcionales. En el ejemplo, el siguiente código define el bucle for
infinito:
for ( ; ; )
{
//...
}
Instrucción foreach
La instrucción foreach ejecuta una instrucción o un bloque de instrucciones para cada elemento de una
instancia del tipo que implementa la interfaz System.Collections.IEnumerable o
System.Collections.Generic.IEnumerable<T>, como se muestra en el siguiente ejemplo:
En el siguiente ejemplo se usa la instrucción foreach con una instancia del tipo System.Span<T>, que no
implementa ninguna interfaz:
A partir de C# 7.3, si la propiedad Current del enumerador devuelve un valor devuelto de referencia ( ref T
donde T es el tipo de un elemento de colección), se puede declarar una variable de iteración con el modificador
ref o ref readonly , como se muestra en el siguiente ejemplo:
También puede usar la instrucción await foreach con una instancia de cualquier tipo que cumpla las
condiciones siguientes:
Un tipo tiene el método público GetAsyncEnumerator sin parámetros. Ese método puede ser el método de
extensión del tipo.
El tipo de valor devuelto del método GetAsyncEnumerator tiene la propiedad pública Current y el método
público MoveNextAsync sin parámetros cuyo tipo de valor devuelto es Task<bool> , ValueTask<bool> , o
cualquier otro tipo que se puede esperar cuyo método GetResult de awaiter devuelve un valor bool .
También puede especificar de forma explícita el tipo de una variable de iteración, como se muestra en el código
siguiente:
En el formulario anterior, el tipo T de un elemento de colección se debe poder convertir de forma implícita o
explícita en el tipo V de una variable de iteración. Si se produce un error en una conversión explícita de T en
V en tiempo de ejecución, la instrucción foreach produce InvalidCastException. Por ejemplo, si T es un tipo
de clase no sellada, V puede ser cualquier tipo de interfaz, incluso uno que T no implemente. En tiempo de
ejecución, el tipo de un elemento de colección puede ser el que deriva de T y realmente implementa V . Si ese
no es el caso, se produce InvalidCastException.
Instrucción do
La instrucción do ejecuta una instrucción o un bloque de instrucciones mientras que una expresión booleana
especificada se evalúa como true . Como esa expresión se evalúa después de cada ejecución del bucle, un bucle
do se ejecuta una o varias veces. Esto es diferente de un bucle while, que se ejecuta cero o varias veces.
int n = 0;
do
{
Console.Write(n);
n++;
} while (n < 5);
// Output:
// 01234
Instrucción while
La instrucción while ejecuta una instrucción o un bloque de instrucciones mientras que una expresión
booleana especificada se evalúa como true . Como esa expresión se evalúa antes de cada ejecución del bucle,
un bucle while se ejecuta cero o varias veces. Esto es diferente de un bucle do que se ejecuta una o varias
veces.
En el ejemplo siguiente se muestra el uso de la instrucción while :
int n = 0;
while (n < 5)
{
Console.Write(n);
n++;
}
// Output:
// 01234
Para obtener más información sobre de las características agregadas en C# 8.0 y versiones posteriores, vea las
siguientes notas de propuesta de características:
Flujos asincrónicos (C# 8.0)
Compatibilidad con extensiones GetEnumerator para bucles foreach (C# 9.0)
Vea también
Referencia de C#
Utilizar foreach con matrices
Iteradores
Instrucciones de selección (referencia de C#)
16/09/2021 • 5 minutes to read
Las instrucciones siguientes seleccionan las instrucciones que se ejecutarán a partir de una serie de
instrucciones posibles en función del valor de una expresión:
La instrucción if selecciona una instrucción para ejecutarla en función del valor de una expresión booleana.
La instrucción switch selecciona una lista de instrucciones para ejecutarla en función de la coincidencia de
un patrón con una expresión.
Instrucción if
Una instrucción if puede tener cualquiera de las dos formas siguientes:
Una instrucción if con una parte else selecciona una de las dos instrucciones que se ejecutarán en
función del valor de una expresión booleana, como se muestra en el ejemplo siguiente:
Una instrucción if sin una parte else ejecuta el cuerpo solo si una expresión booleana se evalúa como
true , como se muestra en el ejemplo siguiente:
Puede anidar instrucciones if para comprobar varias condiciones, como se muestra en el ejemplo siguiente:
DisplayCharacter('f'); // Output: A lowercase letter: f
DisplayCharacter('R'); // Output: An uppercase letter: R
DisplayCharacter('8'); // Output: A digit: 8
DisplayCharacter(','); // Output: Not alphanumeric character: ,
En un contexto de expresión, puede usar el operador condicional ?: para evaluar una de las dos expresiones en
función del valor de una expresión booleana.
Instrucción switch
La instrucción switch selecciona una lista de instrucciones para ejecutarla en función de la coincidencia de un
patrón con una expresión de coincidencia, como se muestra en el ejemplo siguiente:
case double.NaN:
Console.WriteLine("Failed measurement.");
break;
default:
Console.WriteLine($"Measured value is {measurement}.");
break;
}
}
IMPORTANT
Para obtener información sobre los patrones admitidos por la instrucción switch , consulte Patrones.
En el ejemplo anterior también se muestra el caso default . El caso default especifica las instrucciones que se
ejecutarán cuando una expresión de coincidencia no coincida con ningún otro patrón de caso. Si una expresión
de coincidencia no coincide con ningún patrón de caso y no hay ningún caso default , el control pasa por una
instrucción switch .
Una instrucción switch ejecuta la lista de instrucciones en la primera sección de switch cuyo patrón de caso
coincida con una expresión de coincidencia y cuya restricción de caso, de haberla, se evalúe como true . Una
instrucción switch evalúa los patrones de casos en el orden de texto de arriba a abajo. El compilador genera un
error cuando una instrucción switch contiene un caso inaccesible. Ese es un caso que ya se controla mediante
un caso superior o cuyo patrón es imposible de hacer coincidir.
NOTE
El caso default puede aparecer en cualquier lugar de una instrucción switch . Independientemente de su posición, el
caso default siempre se evalúa por último y solo si no coinciden todos los demás patrones de caso.
Puede especificar varios patrones de casos para una sección de una instrucción switch , como se muestra en el
ejemplo siguiente:
default:
Console.WriteLine($"Measured value is {measurement}.");
break;
}
}
Dentro de una instrucción switch , el control no puede pasar desde una sección switch a la siguiente. Como se
muestra en los ejemplos de esta sección, normalmente se usa la instrucción break al final de cada sección
switch para pasar el control desde una instrucción switch . También puede usar las instrucciones return y throw
para pasar el control desde una instrucción switch . Para imitar el comportamiento de pasaje explícito y pasar el
control a otra sección switch, puede usar la instrucción goto .
En el contexto de una expresión, puede usar la expresión switch para evaluar una expresión única a partir de
una lista de expresiones candidatas basada en una coincidencia de patrón con una expresión.
Restricciones de mayúsculas y minúsculas
Un patrón de caso puede no ser lo suficientemente expresivo como para especificar la condición para la
ejecución de la sección switch. En tal caso, puede usar una restricción de caso. Se trata de una condición
adicional que debe cumplirse junto con un patrón coincidente. Una restricción de caso debe ser una expresión
booleana. Especifique una restricción de mayúsculas y minúsculas después de la palabra clave when que sigue
un patrón, como se muestra en el ejemplo siguiente:
default:
Console.WriteLine("One or both measurements are not valid.");
break;
}
}
Para más información sobre las características presentadas en C# 7.0 y versiones posteriores, vea las siguientes
notas de propuesta de características:
Instrucción switch (coincidencia de patrones para C# 7.0)
Vea también
Referencia de C#
Operador condicional ?:
Operadores lógicos
Patrones
Expresión switch
Caracteres especiales de C#
16/09/2021 • 2 minutes to read
Los caracteres especiales son caracteres contextuales predefinidos que modifican el elemento de programa (una
cadena literal, un identificador o un nombre de atributo) para que se antepongan. C# admite los siguientes
caracteres especiales:
@, el carácter de identificador textual.
$, el carácter de cadena interpolada.
Consulte también
Referencia de C#
Guía de programación de C#
$ - Interpolación de cadenas: referencia de C#
16/09/2021 • 5 minutes to read
El carácter especial $ identifica un literal de cadena como una cadena interpolada. Una cadena interpolada es
un literal de cadena que puede contener expresiones de interpolación. Cuando una cadena interpolada se
resuelve en una cadena de resultado, los elementos con expresiones de interpolación se reemplazan por las
representaciones de cadena de los resultados de la expresión. Esta característica está disponible a partir de C# 6.
La interpolación de cadenas proporciona una sintaxis más legible y cómoda de crear cadenas con formato que si
se usa la característica de formato compuesto de cadena. En este ejemplo se usan ambas características para
producir el mismo resultado:
// Composite formatting:
Console.WriteLine("Hello, {0}! Today is {1}, it's {2:HH:mm} now.", name, date.DayOfWeek, date);
// String interpolation:
Console.WriteLine($"Hello, {name}! Today is {date.DayOfWeek}, it's {date:HH:mm} now.");
// Both calls produce the same output that is similar to:
// Hello, Mark! Today is Wednesday, it's 19:40 now.
{<interpolationExpression>[,<alignment>][:<formatString>]}
Los elementos entre corchetes son opcionales. En esta tabla se describe cada elemento:
EL EM EN TO DESC RIP C IÓ N
En el ejemplo siguiente, se usan componentes opcionales de formato que se han descrito anteriormente:
Console.WriteLine($"|{"Left",-7}|{"Right",7}|");
A partir de C# 10, se puede utilizar la interpolación de cadenas para inicializar una cadena constante cuando
todas las expresiones utilizadas para los marcadores de posición son también cadenas constantes. En otras
palabras, cada elemento interpolationexpression debe ser una cadena y debe ser una constante en tiempo de
compilación.
Caracteres especiales
Para incluir una llave ("{" o "}") en el texto generado por una cadena interpolada, use dos llaves ("{{" o "}}"). Para
más información, vea Llaves de escape.
Como los dos puntos (":") tienen un significado especial en un elemento de expresión de interpolación, para
poder usar un operador condicional en una expresión de interpolación, incluya esa expresión entre paréntesis.
En este ejemplo, se muestra cómo incluir una llave en una cadena de resultado y cómo usar un operador
condicional en una expresión de interpolación:
Las cadenas textuales interpoladas comienzan por el carácter $ , seguido del carácter @ . Para más información
sobre las cadenas textuales, vea los temas string e Identificador textual.
NOTE
A partir de C# 8.0, puede usar los tokens $ y @ en cualquier orden; tanto $@"..." como @$"..." son cadenas
textuales interpoladas válidas. En versiones de C# anteriores, el token $ debe aparecer delante del token @ .
System.Globalization.CultureInfo.CurrentCulture = System.Globalization.CultureInfo.GetCultureInfo("nl-NL");
string messageInCurrentCulture = message.ToString();
Console.WriteLine($"{System.Globalization.CultureInfo.CurrentCulture,-10} {messageInCurrentCulture}");
Console.WriteLine($"{specificCulture,-10} {messageInSpecificCulture}");
Console.WriteLine($"{"Invariant",-10} {messageInInvariantCulture}");
// Expected output is:
// nl-NL The speed of light is 299.792,458 km/s.
// en-IN The speed of light is 2,99,792.458 km/s.
// Invariant The speed of light is 299,792.458 km/s.
Recursos adicionales
Si no está familiarizado con la interpolación de cadenas, vea el tutorial interactivo Interpolación de cadenas en
C#. También puede consultar otro tutorial de interpolación de cadenas en C# que muestra cómo usar cadenas
interpoladas para generar cadenas con formato.
El carácter especial @ actúa como un identificador textual. Se puede usar de estas formas:
1. Para habilitar el uso de palabras clave de C# como identificadores. El carácter @ actúa como prefijo de
un elemento de código que el compilador debe interpretar como un identificador en lugar de como una
palabra clave de C#. En el ejemplo siguiente se usa el carácter @ para definir un identificador
denominado for que se usa en un bucle for .
2. Para indicar que un literal de cadena se debe interpretar literalmente. El carácter @ de esta instancia
define un literal de cadena textual. Las secuencias de escape sencillas (como "\\" , que es una barra
diagonal inversa), las secuencias de escape hexadecimales (como "\x0041" , que es una A mayúscula) y
las secuencias de escape Unicode (como "\u0041" que es una A mayúscula) se interpretan literalmente.
Solo las secuencias de escape de comillas ( "" ) no se interpretan literalmente, sino que generan comillas
dobles. De igual modo, en el caso de una cadena interpolada literal, las secuencias de escape de llave ( {{
y }} ) no se interpretan literalmente, sino que generan caracteres de llave simple. En el siguiente ejemplo
se definen dos rutas de archivo idénticas, una mediante un literal de cadena normal y otra mediante el
uso de un literal de cadena textual. Este es uno de los usos más comunes de los literales de cadena
textual.
Console.WriteLine(filename1);
Console.WriteLine(filename2);
// The example displays the following output:
// c:\documents\files\u0066.txt
// c:\documents\files\u0066.txt
En el ejemplo siguiente se muestra el efecto de definir un literal de cadena normal y un literal de cadena
textual que contienen secuencias de caracteres idénticos.
Console.WriteLine(s1);
Console.WriteLine(s2);
// The example displays the following output:
// He said, "This is the last chance!"
// He said, "This is the last \u0063hance\x0021"
3. Para permitir que el compilador distinga entre los atributos en caso de conflicto de nomenclatura. Un
atributo es una clase que deriva de Attribute. Normalmente, su nombre de tipo incluye el sufijo
Attribute , aunque el compilador no exige el cumplimiento de esta convención. Es posible hacer
referencia al atributo en el código mediante su nombre de tipo completo (por ejemplo, [InfoAttribute] )
o mediante su nombre abreviado (por ejemplo, [Info] ). Pero se produce un conflicto de nomenclatura si
dos nombres abreviados de tipo de atributo son idénticos, y un nombre de tipo incluye el sufijo
Attribute y el otro no. Por ejemplo, el código siguiente produce un error al compilarse porque el
compilador no puede determinar si el atributo Info o InfoAttribute se aplica a la clase Example . Vea
CS1614 para obtener más información.
using System;
[AttributeUsage(AttributeTargets.Class)]
public class Info : Attribute
{
private string information;
[AttributeUsage(AttributeTargets.Method)]
public class InfoAttribute : Attribute
{
private string information;
[Info("A simple executable.")] // Generates compiler error CS1614. Ambiguous Info and InfoAttribute.
// Prepend '@' to select 'Info' ([@Info("A simple executable.")]). Specify the full name
'InfoAttribute' to select it.
public class Example
{
[InfoAttribute("The entry point.")]
public static void Main()
{
}
}
Vea también
Referencia de C#
Guía de programación de C#
Caracteres especiales de C#
Atributos reservados: atributos de nivel de
ensamblado
16/09/2021 • 2 minutes to read
La mayoría de los atributos se aplican a elementos específicos del lenguaje, como las clases o los métodos,
aunque algunos atributos son globales (se aplican a todo un ensamblado o módulo). Por ejemplo, el atributo
AssemblyVersionAttribute se puede usar para insertar información de versión en un ensamblado, como en este
ejemplo:
[assembly: AssemblyVersion("1.0.0.0")]
Los atributos globales aparecen en el código fuente después de cualquier directiva using de nivel superior y
antes de cualquier declaración de tipo, módulo o espacio de nombres. Los atributos globales pueden aparecer
en varios archivos de código fuente, pero estos archivos se deben compilar en un solo paso de compilación.
Visual Studio agrega atributos globales al archivo AssemblyInfo.cs en proyectos de .NET Framework. Estos
atributos no se agregan a los proyectos de .NET Core.
Los atributos de ensamblado son valores que proporcionan información sobre un ensamblado. Se dividen en
las siguientes categorías:
Atributos de identidad del ensamblado
Atributos informativos
Atributos de manifiesto del ensamblado
Atributos informativos
Puede usar atributos informativos para proporcionar información adicional de la empresa o el producto para un
ensamblado. En la tabla siguiente se muestran los atributos informativos definidos en el espacio de nombres
System.Reflection.
Mediante los atributos de información, se obtiene información sobre el autor de la llamada a un método. Se
obtiene la ruta de acceso al código fuente, el número de línea del código fuente y el nombre de miembro del
autor de la llamada. Para obtener la información del llamador del miembro, use los atributos que se aplican a los
parámetros opcionales. Cada parámetro opcional especifica un valor predeterminado. En la tabla siguiente se
enumeran los atributos de información del llamador que se definen en el espacio de nombres
System.Runtime.CompilerServices:
// Sample Output:
// message: Something happened.
// member name: DoProcessing
// source file path: c:\Visual Studio Projects\CallerInfoCS\CallerInfoCS\Form1.cs
// source line number: 31
Debe especificar un valor predeterminado explícito para cada parámetro opcional. No puede aplicar atributos de
información del autor de la llamada a los parámetros que no se especifican como opcionales. Los atributos de
información del autor de la llamada no crean un parámetro opcional, sino que influyen en el valor
predeterminado que se pasa cuando se omite el argumento. Los valores de información del autor de la llamada
se emiten como literales en el lenguaje intermedio (IL) en tiempo de compilación. A diferencia de los resultados
de la propiedad StackTrace para las excepciones, los resultados no se ven afectados por confusión. Puede
proporcionar explícitamente los argumentos opcionales para controlar la información del llamador u ocultarla.
Nombres de miembro
Se puede utilizar el atributo CallerMemberName para evitar especificar el nombre de miembro como un
argumento String para el método llamado. Mediante esta técnica, se evita el problema de que la
refactorización de cambio de nombre no cambie los valores String . Esta ventaja es especialmente útil
para las siguientes tareas:
Usar el seguimiento y las rutinas de diagnóstico.
Implementar la interfaz INotifyPropertyChanged al enlazar datos. Esta interfaz permite que la propiedad de
un objeto notifique a un control enlazado que la propiedad ha cambiado, de forma que el control pueda
mostrar información actualizada. Sin el atributo CallerMemberName , se debe especificar el nombre de
propiedad como un literal.
En el gráfico siguiente se muestran los nombres de miembro que se devuelven cuando se utiliza el atributo
CallerMemberName .
Conversiones u operadores definidos por el usuario Nombre generado para el miembro, por ejemplo,
"op_Addition".
Ningún miembro contenedor (por ejemplo, nivel de El valor predeterminado del parámetro opcional.
ensamblado o atributos que se aplican a tipos)
Vea también
Argumentos opcionales y con nombre
System.Reflection
Attribute
Atributos
Atributos para el análisis estático de estado NULL
16/09/2021 • 17 minutes to read
En un contexto que admite un valor NULL, el compilador realiza un análisis estático del código para determinar
el estado NULL de todas las variables de tipo de referencia:
not null: el análisis estático determina que a la variable se le asigna un valor distinto de NULL.
maybe null: el análisis estático no puede determinar que a la variable se le asigna un valor distinto de NULL.
Puede aplicar atributos que proporcionan información al compilador sobre la semántica de las API. Estos
atributos ayudan a definir el contrato de nulabilidad para la API. El contrato ayuda al compilador a realizar
análisis estáticos de cualquier código que llame a la API. Por ejemplo, si el compilador determina que una
variable puede ser NULL y el código no comprueba que antes de eliminar la referencia de la variable, emite una
advertencia.
En este artículo se proporciona una breve descripción de cada uno de los atributos de tipo de referencia que
acepta valores NULL y cómo usarlos. En todos los ejemplos se asume el uso de C# 8.0 o una versión más
reciente, y que el código se encuentra en un contexto que admite un valor NULL.
Comencemos con un ejemplo. Imagine que la biblioteca tiene la siguiente API para recuperar una cadena de
recursos:
En el ejemplo anterior se sigue el conocido patrón de Try* en .NET. Hay dos argumentos de referencia para
esta API: los parámetros key y message . Esta API tiene las siguientes reglas relacionadas con la obtención del
valor NULL de estos argumentos:
Los autores de la llamada no deben pasar null como argumento para key .
Los autores de la llamada pueden pasar una variable cuyo valor sea null como argumento de message .
Si el método TryGetMessage devuelve true , el valor de message no es NULL. Si el valor devuelto es false, ,
el valor de message (y su estado NULL) es NULL.
La regla para key se puede expresar mediante el tipo de variable: key debe ser un tipo de referencia que no
acepte valores NULL. El parámetro message es más complejo. Permite null como argumento, pero garantiza
que, si se ejecuta correctamente, el argumento out no sea NULL. En estos escenarios, necesita un vocabulario
más completo para describir las expectativas.
C# 8 introdujo varios atributos para expresar información adicional sobre el estado NULL de las variables. Todo
el código escrito antes de que C# 8 introdujera los tipos de referencia que aceptan valores NULL desconocía los
valores NULL. Esto significa que es posible que cualquier variable de tipo de referencia sea NULL, pero no se
necesitan comprobaciones de valores NULL. Una vez que el código admite valores NULL, esas reglas cambian.
Los tipos de referencia nunca deben ser del valor null , y los que admitan valores NULL se deben comprobar
con null antes de desreferenciarlos.
Es probable que las reglas de las API sean más complicadas, como se ha visto en el escenario de la API
TryGetValue . Muchas de las API tienen reglas más complejas para cuando las variables pueden ser null o no.
En estos casos, usará uno de los atributos de la siguiente tabla para expresar estas reglas.
NOTE
Al agregar estos atributos, se proporciona más información al compilador sobre las reglas de la API. Cuando el código que
realiza la llamada se compila en un contexto habilitado para aceptar valores NULL, el compilador advertirá a los autores de
la llamada cuando infrinjan esas reglas. Estos atributos no habilitan más comprobaciones en la implementación.
Las descripciones anteriores son una referencia rápida a lo que hace cada atributo. En las secciones siguientes se
describe el comportamiento y el significado de estos atributos de forma más exhaustiva.
Condiciones previas: AllowNull y DisallowNull
Considere una propiedad de lectura y escritura que nunca devuelve null porque tiene un valor
predeterminado razonable. Los autores de la llamada pasan null al descriptor de acceso set cuando lo
establecen en el valor predeterminado. Por ejemplo, imagine un sistema de mensajería que solicita un nombre
de pantalla en un salón de chat. Si no se proporciona ninguno, el sistema genera uno aleatorio:
Al compilar el código anterior en un contexto en el que se desconocen los valores NULL, todo es correcto. Una
vez que se habilitan los tipos de referencia que admiten un valor NULL, la propiedad ScreenName se convierte
en una referencia que no acepta valores NULL. Eso es correcto para el descriptor de acceso get : nunca
devuelve null . No es necesario que los autores de la llamada comprueben null en la propiedad devuelta.
Pero ahora, al establecer la propiedad en null , se genera una advertencia. Para admitir este tipo de código,
agregue el atributo System.Diagnostics.CodeAnalysis.AllowNullAttribute a la propiedad, como se muestra en el
código siguiente:
[AllowNull]
public string ScreenName
{
get => _screenName;
set => _screenName = value ?? GenerateRandomScreenName();
}
private string _screenName = GenerateRandomScreenName();
Es posible que tenga que agregar una directiva using para System.Diagnostics.CodeAnalysis a fin de usar este
y otros atributos descritos en este artículo. El atributo se aplica a la propiedad, no al descriptor de acceso set .
El atributo AllowNull especifica condiciones previas y solo se aplica a los argumentos. El descriptor de acceso
get tiene un valor devuelto, pero no parámetros. Por tanto, el atributo AllowNull solo se aplica al descriptor de
acceso set .
En el ejemplo anterior se muestra qué se debe buscar al agregar el atributo AllowNull en un argumento:
1. El contrato general para esa variable es que no debe ser null , por lo que quiere un tipo de referencia que
no acepte valores NULL.
2. Hay escenarios en los que la variable de entrada es null , aunque no sean el uso más común.
En la mayoría de los casos necesitará este atributo para las propiedades, o bien para los argumentos in , out y
ref . El atributo AllowNull es la mejor opción cuando una variable normalmente no es NULL, pero debe
permitir null como condición previa.
Compare esto con los escenarios donde se usa DisallowNull : este atributo se usa para especificar que un
argumento de un tipo de referencia que admite un valor NULL no deba ser null . Considere una propiedad
donde null es el valor predeterminado, pero los clientes solo pueden establecerla en un valor que no sea
NULL. Observe el código siguiente:
public string ReviewComment
{
get => _comment;
set => _comment = value ?? throw new ArgumentNullException(nameof(value), "Cannot set to null");
}
string _comment;
El código anterior es la mejor manera de expresar el diseño de que ReviewComment pueda ser null , pero no se
puede establecer en null . Una vez que este código admita valores NULL, puede expresar este concepto con
más claridad para los autores de la llamada mediante System.Diagnostics.CodeAnalysis.DisallowNullAttribute:
[DisallowNull]
public string? ReviewComment
{
get => _comment;
set => _comment = value ?? throw new ArgumentNullException(nameof(value), "Cannot set to null");
}
string? _comment;
En un contexto que admite un valor NULL, el descriptor de acceso get de ReviewComment podría devolver el
valor predeterminado de null . El compilador advierte que se debe comprobar antes del acceso. Además,
advierte a los autores de la llamada que, aunque podría ser null , no deberían establecerlo de forma explícita
en null . El atributo DisallowNull también especifica una condición previa, no afecta al descriptor de acceso
get . Use el atributo DisallowNull cuando observe estas características:
1. La variable podría ser null en escenarios principales, a menudo cuando se crea su primera instancia.
2. La variable no se debe establecer de forma explícita en null .
Estas situaciones son comunes en el código en el que originalmente se desconocían los valores NULL. Es posible
que las propiedades de objeto se establezcan en dos operaciones de inicialización distintas. Es posible que
algunas propiedades se establezcan solo después de que se haya completado algún trabajo asincrónico.
Los atributos AllowNull y DisallowNull permiten especificar que las condiciones previas de las variables
puedan no coincidir con las anotaciones que admiten un valor NULL en esas variables. Proporcionan más
detalles sobre las características de la API. Esta información adicional ayuda a los autores de la llamada a usar la
API de manera correcta. Recuerde que debe especificar las condiciones previas mediante los atributos
siguientes:
AllowNull: un argumento que no acepta valores NULL puede ser NULL.
DisallowNull: un argumento que admite un valor NULL nunca debe ser NULL.
Probablemente haya escrito un método como este para devolver null cuando no se encuentra el nombre que
se busca. null indica claramente que no se ha encontrado el registro. En este ejemplo, es probable que cambie
el tipo de valor devuelto de Customer a Customer? . Al declarar el valor devuelto como un tipo de referencia que
admite un valor NULL, se especifica claramente la intención de esta API.
Por los motivos descritos en Definiciones genéricas y nulabilidad, esa técnica no funciona con los métodos
genéricos. Puede tener un método genérico que siga un patrón similar:
public T Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate)
No puede especificar que el valor devuelto sea T? . El método devuelve null cuando no se encuentra el
elemento buscado. Como no puede declarar un tipo de valor devuelto T? , agregue la anotación MaybeNull al
valor devuelto del método:
[return: MaybeNull]
public T Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate)
El código anterior informa a los autores de la llamada de que el contrato implica un tipo que no acepta valores
NULL, pero el valor devuelto puede realmente ser NULL. Use el atributo MaybeNull cuando la API deba ser un
tipo que no acepta valores NULL, normalmente un parámetro de tipo genérico, pero puede haber casos en los
que se devuelva null .
También puede especificar que un valor devuelto o un argumento no sean NULL aunque el tipo sea un tipo de
referencia que admite un valor NULL. El método siguiente es un método auxiliar que se produce si su primer
argumento es null :
Console.WriteLine(message.Length);
}
Después de habilitar los tipos de referencia NULL, querrá asegurarse de que el código anterior se compila sin
advertencias. Cuando el método devuelve un valor, se garantiza que el argumento value no es NULL. Pero es
aceptable llamar a ThrowWhenNull con una referencia nula. Puede convertir a value en un tipo de referencia
que acepte valores NULL y agregar la condición posterior NotNull a la declaración del parámetro:
En el código anterior se expresa con claridad el contrato existente: los autores de la llamada pueden pasar una
variable con el valor null , pero se garantiza que el valor devuelto nunca será NULL.
Las condiciones posteriores condicionales se especifican mediante los atributos siguientes:
MaybeNull: un valor devuelto que no acepta valores NULL puede ser NULL.
NotNull: un valor devuelto que admite un valor NULL nunca será NULL.
Esto informa al compilador de que no es necesario comprobar los valores NULL en el código cuyo valor
devuelto sea false . La adición del atributo informa al análisis estático del compilador que IsNullOrEmpty
realiza la comprobación de valores NULL necesaria: cuando devuelve false , el argumento no es null .
El método String.IsNullOrEmpty(String) se anotará como se ha mostrado antes para .NET Core 3.0. Es posible
que tenga métodos similares en el código base que comprueben valores NULL en el estado de los objetos. El
compilador no reconocerá los métodos de comprobación de valores NULL personalizados y tendrá que agregar
personalmente las anotaciones. Al agregar el atributo, el análisis estático del compilador sabe cuándo se han
comprobado los valores NULL en la variable.
Otro uso de estos atributos es el patrón Try* . Las condiciones posteriores para las variables ref y out se
comunican a través del valor devuelto. Observe este método mostrado antes:
El método anterior sigue una expresión de .NET típica: el valor devuelto indica si message se ha establecido en el
valor encontrado o, si no se ha encontrado ningún mensaje, en el valor predeterminado. Si el método devuelve
true , el valor de message no es NULL; de lo contrario, el método establece message en NULL.
Puede comunicar esa expresión mediante el atributo NotNullWhen . Al actualizar la firma para los tipos de
referencia que admiten un valor NULL, convierta message en string? y agregue un atributo:
En el ejemplo anterior, se sabe que el valor de message no es NULL cuando TryGetMessage devuelve true .
Debe anotar de la misma forma los métodos similares en el código base: los argumentos pueden ser null , y se
sabe que no son NULL cuando el método devuelve true .
Hay un atributo final que también puede necesitar. En ocasiones, el estado NULL de un valor devuelto depende
del estado NULL de uno o más argumentos. Estos métodos devolverán un valor no NULL siempre que
determinados argumentos no sean null . Para anotar correctamente estos métodos, use el atributo
NotNullIfNotNull . Observe el método siguiente:
Si el argumento url no es NULL, el resultado no es null . Una vez que se hayan habilitado las referencias
nulas, esa firma funcionará correctamente, siempre que la API no acepte nunca un argumento NULL. Pero si el
argumento puede ser NULL, el valor devuelto también podría serlo. Podría cambiar la firma por el código
siguiente:
Esto también funciona, pero a menudo obliga a los autores de la llamada a implementar comprobaciones de
null adicionales. El contrato es que el valor devuelto solo será null cuando el argumento url sea null .
Para expresar ese contrato, tendría que anotar este método como se muestra en el código siguiente:
[return: NotNullIfNotNull("url")]
string? GetTopLevelDomainFromFullUrl(string? url)
El valor devuelto y el argumento se han anotado con ? , lo que indica que cualquiera podría ser null . El
atributo aclara todavía más que el valor devuelto no será NULL cuando el argumento url no sea null .
Las condiciones posteriores condicionales se especifican mediante estos atributos:
MaybeNullWhen: un argumento que no acepta valores NULL puede ser NULL cuando el método devuelve el
valor bool especificado.
NotNullWhen: un argumento que admite un valor NULL nunca será NULL cuando el método devuelva el
valor bool especificado.
NotNullIfNotNull: un valor devuelto no es NULL si el argumento del parámetro especificado no es NULL.
public Container()
{
Helper();
}
[MemberNotNull(nameof(_uniqueIdentifier))]
private void Helper()
{
_uniqueIdentifier = DateTime.Now.Ticks.ToString();
}
}
Puede especificar varios nombres de campo como argumentos para el constructor de atributo MemberNotNull .
MemberNotNullWhenAttribute tiene un argumento bool . Utiliza MemberNotNullWhen en situaciones en las que
el método auxiliar devuelve bool , lo cual indica si el método auxiliar ha inicializado los campos.
[DoesNotReturn]
private void FailFast()
{
throw new InvalidOperationException();
}
En el segundo caso, se agrega el atributo DoesNotReturnIf a un parámetro booleano del método. Puede
modificar el ejemplo anterior de esta manera:
private void FailFastIf([DoesNotReturnIf(true)] bool isNull)
{
if (isNull)
{
throw new InvalidOperationException();
}
}
Resumen
IMPORTANT
La documentación oficial hace un seguimiento de la versión más reciente de C#. Actualmente, estamos escribiendo para
C# 9.0. Dependiendo de la versión de C# que use, puede que no estén disponibles ciertas características. La versión
predeterminada de C# para el proyecto se basa en la plataforma de destino. Para obtener más información, vea las
versiones predeterminadas del lenguaje C#.
Agregar tipos de referencia que aceptan valores NULL proporciona un vocabulario inicial para describir las
expectativas de las API para las variables que podrían ser null . Los atributos proporcionan un vocabulario más
completo para describir el estado NULL de las variables como condiciones previas y posteriores. Estos atributos
describen con más claridad las expectativas y proporcionan una mejor experiencia para los desarrolladores que
usan las API.
A medida que actualice las bibliotecas para un contexto que admite un valor NULL, agregue estos atributos para
guiar a los usuarios de las API al uso correcto. Estos atributos ayudan a describir de forma completa el estado
NULL de los argumentos y los valores devueltos:
AllowNull: un argumento que no acepta valores NULL puede ser NULL.
DisallowNull: un argumento que admite un valor NULL nunca debe ser NULL.
MaybeNull: un valor devuelto que no acepta valores NULL puede ser NULL.
NotNull: un valor devuelto que admite un valor NULL nunca será NULL.
MaybeNullWhen: un argumento que no acepta valores NULL puede ser NULL cuando el método devuelve el
valor bool especificado.
NotNullWhen: un argumento que admite un valor NULL nunca será NULL cuando el método devuelva el
valor bool especificado.
NotNullIfNotNull: un valor devuelto no es NULL si el argumento del parámetro especificado no es NULL.
DoesNotReturn: un método nunca devuelve un valor. Es decir, siempre inicia una excepción.
DoesNotReturnIf: este método nunca devuelve un valor si el parámetro bool asociado tiene el valor
especificado.
Atributos reservados: varios
16/09/2021 • 10 minutes to read
Estos atributos se pueden aplicar a los elementos del código. Agregan significado semántico a esos elementos.
El compilador usa esos significados semánticos para modificar su salida e informar de los posibles errores por
parte de los desarrolladores que usan el código.
Atributo Conditional
El atributo Conditional hace que la ejecución de un método dependa de un identificador de preprocesamiento.
El atributo Conditional es un alias de ConditionalAttribute y se puede aplicar a un método o a una clase de
atributo.
En el ejemplo siguiente, Conditional se aplica a un método para habilitar o deshabilitar la representación de
información de diagnóstico específica del programa:
#define TRACE_ON
using System;
using System.Diagnostics;
namespace AttributeExamples
{
public class Trace
{
[Conditional("TRACE_ON")]
public static void Msg(string msg)
{
Console.WriteLine(msg);
}
}
Si no se define el identificador TRACE_ON , no se muestra el resultado del seguimiento. Explore por sí mismo en la
ventana interactiva.
El atributo Conditional se suele usar con el identificador DEBUG para habilitar las funciones de seguimiento y
de registro para las compilaciones de depuración, pero no en las compilaciones de versión, como se muestra en
el ejemplo siguiente:
[Conditional("DEBUG")]
static void DebugMethod()
{
}
Al llamar a un método marcado como condicional, la presencia o ausencia del símbolo de preprocesamiento
especificado determina si el compilador incluye u omite la llamada al método. Si el símbolo está definido, se
incluye la llamada; de lo contrario, se omite la llamada. Un método condicional debe ser un método de una
declaración de clase o estructura, y no debe tener un tipo de valor devuelto void . El uso de Conditional resulta
más limpio y elegante, y menos propenso a generar errores que incluir los métodos dentro de bloques
#if…#endif .
Si un método tiene varios atributos Conditional , el compilador incluye llamadas al método si se define uno o
más símbolos condicionales (los símbolos se vinculan de manera lógica entre sí mediante el operador OR). En el
ejemplo siguiente, la presencia de A o B da como resultado una llamada al método:
[Conditional("A"), Conditional("B")]
static void DoIfAorB()
{
// ...
}
[Conditional("DEBUG")]
public class DocumentationAttribute : System.Attribute
{
string text;
class SampleClass
{
// This attribute will only be included if DEBUG is defined.
[Documentation("This method displays an integer.")]
static void DoWork(int i)
{
System.Console.WriteLine(i.ToString());
}
}
Atributo Obsolete
El atributo Obsolete marca un elemento de código como ya no recomendado para su uso. El uso de una
entidad marcada como obsoleta genera una advertencia o un error. El atributo Obsolete es un atributo de uso
único y se puede aplicar a cualquier entidad que admita atributos. Obsolete es un alias de ObsoleteAttribute.
En el ejemplo siguiente, el atributo Obsolete se aplica a la clase A y al método B.OldMethod . Dado que el
segundo argumento del constructor de atributos aplicado a B.OldMethod está establecido en true , este método
producirá un error del compilador, mientras que, si se usa la clase A , solo se generará una advertencia. En
cambio, si se llama a B.NewMethod , no se generará ninguna advertencia o error. Por ejemplo, al usarla con las
definiciones anteriores, el código siguiente genera dos advertencias y un error:
using System;
namespace AttributeExamples
{
[Obsolete("use class B")]
public class A
{
public void Method() { }
}
public class B
{
[Obsolete("use NewMethod", true)]
public void OldMethod() { }
La cadena proporcionada como primer argumento al constructor del atributo se mostrará como parte de la
advertencia o el error. Se generan dos advertencias para la clase A : una para la declaración de la referencia de
clase y otra para el constructor de clases. El atributo Obsolete se puede usar sin argumentos, pero se
recomienda incluir una explicación de qué se debe usar en su lugar.
En C# 10, puede usar la interpolación de cadenas constantes y el operador nameof para asegurarse de que los
nombres coinciden:
public class B
{
[Obsolete($"use {nameof(NewMethod)} instead", true)]
public void OldMethod() { }
Atributo AttributeUsage
El atributo AttributeUsage determina cómo se puede usar una clase de atributo personalizado.
AttributeUsageAttribute es un atributo que se aplica a las definiciones de atributo personalizado. El atributo
AttributeUsage permite controlar lo siguiente:
Los atributos de elementos de programa que se pueden aplicar. A menos que el uso esté restringido, un
atributo se puede aplicar a cualquiera de los siguientes elementos de programa:
ensamblado
module
campo
event
método
param
propiedad
return
type
Si un atributo se puede aplicar a un mismo elemento de programa varias veces.
Si las clases derivadas heredan atributos.
La configuración predeterminada se parece al siguiente ejemplo cuando se aplica explícitamente:
[AttributeUsage(AttributeTargets.All,
AllowMultiple = false,
Inherited = true)]
class NewAttribute : Attribute { }
En este ejemplo, la clase NewAttribute se puede aplicar a cualquier elemento de programación compatible, pero
solamente se puede aplicar una vez a cada entidad. Las clases derivadas heredan el atributo cuando se aplica a
una clase base.
Los argumentos AllowMultiple y Inherited son opcionales, por lo que el siguiente código tiene el mismo efecto:
[AttributeUsage(AttributeTargets.All)]
class NewAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
class NewPropertyOrFieldAttribute : Attribute { }
A partir de C# 7.3, los atributos se pueden aplicar a la propiedad o el campo de respaldo de una propiedad
implementada automáticamente. El atributo se aplica a la propiedad, a menos que se indique el especificador
field en el atributo. Ambos se muestran en el siguiente ejemplo:
class MyClass
{
// Attribute attached to property:
[NewPropertyOrField]
public string Name { get; set; } = string.Empty;
Si el argumento AllowMultiple es true , el atributo resultante se puede aplicar más de una vez a cada una de las
entidades, como se muestra en el siguiente ejemplo:
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
class MultiUse : Attribute { }
[MultiUse]
[MultiUse]
class Class1 { }
[MultiUse, MultiUse]
class Class2 { }
En este caso, MultiUseAttribute se puede aplicar varias veces porque AllowMultiple está establecido en true .
Los dos formatos mostrados para aplicar varios atributos son válidos.
Si Inherited se establece en false , las clases que se derivan de una clase con atributos no heredan el atributo.
Por ejemplo:
[NonInherited]
class BClass { }
Atributo AsyncMethodBuilder
A partir de C# 7, se agrega el atributo System.Runtime.CompilerServices.AsyncMethodBuilderAttribute a un tipo
que puede ser un tipo de valor devuelto asincrónico. El atributo especifica el tipo que compila la implementación
del método asincrónico cuando se devuelve el tipo especificado desde un método asincrónico. El atributo
AsyncMethodBuilder se puede aplicar a un tipo que:
Para obtener información sobre los generadores de métodos asincrónicos, lea sobre los generadores siguientes
proporcionados por .NET:
System.Runtime.CompilerServices.AsyncTaskMethodBuilder
System.Runtime.CompilerServices.AsyncTaskMethodBuilder<TResult>
System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder
System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder<TResult>
En C# 10.0 y versiones posteriores, el atributo AsyncMethodBuilder se puede aplicar a un método asincrónico
para invalidar el generador de ese tipo.
Atributo ModuleInitializer
A partir de C# 9, el atributo ModuleInitializer marca un método al que el entorno de ejecución llama cuando
se carga el ensamblado. ModuleInitializer es un alias de ModuleInitializerAttribute.
El atributo ModuleInitializer solo se puede aplicar a un método que:
Sea estático.
No tenga parámetros.
Devuelve void .
Sea accesible desde el módulo contenedor, es decir, internal o public .
No sea un método genérico.
No esté incluido en una clase genérica.
No sea una función local.
El atributo ModuleInitializer se puede aplicar a varios métodos. En ese caso, el orden de llamada desde el
entorno de ejecución es determinista pero no se especifica.
En el ejemplo siguiente se muestra el uso de varios métodos de inicializador de módulo. Los métodos Init1 y
Init2 se ejecutan antes que Main y cada uno agrega una cadena a la propiedad Text . Por tanto, cuando se
ejecuta Main , la propiedad Text ya tiene cadenas de los dos métodos de inicializador.
using System;
using System.Runtime.CompilerServices;
[ModuleInitializer]
public static void Init1()
{
Text += "Hello from Init1! ";
}
[ModuleInitializer]
public static void Init2()
{
Text += "Hello from Init2! ";
}
}
En ocasiones los generadores de código deben generar código de inicialización. Los inicializadores de módulos
proporcionan una ubicación estándar para ese código.
Atributo SkipLocalsInit
A partir de C# 9, el atributo SkipLocalsInit impide que el compilador establezca la marca .locals init
cuando se realiza la emisión a metadatos. SkipLocalsInit es un atributo de uso único y se puede aplicar a un
método, una propiedad, una clase, una estructura, una interfaz o un módulo, pero no a un ensamblado.
SkipLocalsInit es un alias de SkipLocalsInitAttribute.
La marca .locals init hace que el CLR inicialice todas las variables locales declaradas en un método en sus
valores predeterminados. Como el compilador también se asegura de que nunca se use una variable antes de
asignarle un valor, .locals init no suele ser necesario. Pero la inicialización en cero adicional puede afectar al
rendimiento en algunos escenarios, como cuando se usa stackalloc para asignar una matriz en la pila. En esos
casos, puede agregar el atributo SkipLocalsInit . Si se aplica directamente a un método, el atributo afecta a ese
método y a todas sus funciones anidadas, incluidas las expresiones lambda y las funciones locales. Si se aplica a
un tipo o un módulo, afecta a todos los métodos anidados. Este atributo no afecta a los métodos abstractos,
pero sí al código generado para la implementación.
Este atributo necesita la opción del compilador AllowUnsafeBlocks. Esto permite indicar que, en algunos casos,
el código podría ver memoria sin asignar (por ejemplo, leer de la memoria asignada a la pila sin inicializar).
En el ejemplo siguiente se muestra el efecto del atributo SkipLocalsInit en un método que usa stackalloc . El
método muestra lo que hubiera en la memoria al asignar la matriz de enteros.
[SkipLocalsInit]
static void ReadUninitializedMemory()
{
Span<int> numbers = stackalloc int[120];
for (int i = 0; i < 120; i++)
{
Console.WriteLine(numbers[i]);
}
}
// output depends on initial contents of memory, for example:
//0
//0
//0
//168
//0
//-1271631451
//32767
//38
//0
//0
//0
//38
// Remaining rows omitted for brevity.
Para probar este código, establezca la opción del compilador AllowUnsafeBlocks en el archivo .csproj:
<PropertyGroup>
...
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
Vea también
Attribute
System.Reflection
Atributos
Reflexión
Código no seguro, tipos de puntero y punteros de
función
16/09/2021 • 14 minutes to read
La mayor parte del código de C# que se escribe es "código seguro comprobable". El código seguro comprobable
significa que las herramientas de .NET pueden comprobar que el código es seguro. En general, el código seguro
no accede directamente a la memoria mediante punteros. Tampoco asigna memoria sin procesar. En su lugar,
crea objetos administrados.
C# admite un contexto unsafe , en el que se puede escribir código no comprobable. En un contexto unsafe , el
código puede usar punteros, asignar y liberar bloques de memoria y llamar a métodos mediante punteros de
función. El código no seguro en C# no es necesariamente peligroso; solo es código cuya seguridad no se puede
comprobar.
El código no seguro tiene las propiedades siguientes:
Los métodos, tipos y bloques de código se pueden definir como no seguros.
En algunos casos, el código no seguro puede aumentar el rendimiento de la aplicación al eliminar las
comprobaciones de límites de matriz.
El código no seguro es necesario al llamar a funciones nativas que requieren punteros.
El código no seguro presenta riesgos para la seguridad y la estabilidad.
El código que contenga bloques no seguros deberá compilarse con la opción del compilador
AllowUnsafeBlocks .
Tipos de puntero
En un contexto no seguro, un tipo puede ser un tipo de puntero, además de un tipo de valor o un tipo de
referencia. Una declaración de tipos de puntero toma una de las siguientes formas:
type* identifier;
void* identifier; //allowed but not recommended
El tipo especificado antes de * en un tipo de puntero se denomina tipo referente . Solo un tipo no
administrado puede ser un tipo de referente.
Los tipos de puntero no heredan de object y no existe ninguna conversión entre tipos de puntero y object .
Además, las conversiones boxing y unboxing no admiten punteros. Sin embargo, puede realizar la conversión
entre diferentes tipos de puntero y entre tipos de puntero y tipos enteros.
Cuando declare varios punteros en la misma declaración, únicamente debe escribir el asterisco ( * ) con el tipo
subyacente. No se usa como prefijo en cada nombre de puntero. Por ejemplo:
Un puntero no puede señalar a una referencia ni a un struct que contenga referencias, porque una referencia de
objeto puede recolectarse como elemento no utilizado aunque haya un puntero que la señale. El recolector de
elementos no utilizados no realiza un seguimiento de si algún tipo de puntero señala a un objeto.
El valor de la variable de puntero de tipo MyType* es la dirección de una variable de tipo MyType .A
continuación se muestran ejemplos de declaraciones de tipos de puntero:
int* p : es un puntero a un entero.
p
int** p : p es un puntero a un puntero a un entero.
int*[] p : p es una matriz unidimensional de punteros a enteros.
char* p : p es un puntero a un valor char.
void* p : p es un puntero a un tipo desconocido.
El operador de direccionamiento indirecto del puntero * puede usarse para acceder al contenido de la
ubicación señalada por la variable de puntero. Por ejemplo, consideremos la siguiente declaración:
int* myVariable;
La expresión *myVariable denota la variable int que se encuentra en la dirección contenida en myVariable .
Hay varios ejemplos de punteros en los artículos sobre la instrucción fixed . En el ejemplo siguiente se usa la
palabra clave unsafe y la instrucción fixed y se muestra cómo incrementar un puntero interior. Puede pegar
este código en la función Main de una aplicación de consola para ejecutarla. Estos ejemplos se deben compilar
con el conjunto de opciones del compilador AllowUnsafeBlocks .
// Normal pointer to an object.
int[] a = new int[5] { 10, 20, 30, 40, 50 };
// Must be in unsafe code to use interior pointers.
unsafe
{
// Must pin object on heap so that it doesn't move while using interior pointers.
fixed (int* p = &a[0])
{
// p is pinned as well as object, so create another pointer to show incrementing it.
int* p2 = p;
Console.WriteLine(*p2);
// Incrementing p2 bumps the pointer by four bytes due to its type ...
p2 += 1;
Console.WriteLine(*p2);
p2 += 1;
Console.WriteLine(*p2);
Console.WriteLine("--------");
Console.WriteLine(*p);
// Dereferencing p and incrementing changes the value of a[0] ...
*p += 1;
Console.WriteLine(*p);
*p += 1;
Console.WriteLine(*p);
}
}
Console.WriteLine("--------");
Console.WriteLine(a[0]);
/*
Output:
10
20
30
--------
10
11
12
--------
12
*/
No se puede aplicar el operador de direccionamiento indirecto a un puntero de tipo void* . Sin embargo, es
posible usar una conversión para convertir un puntero void en cualquier otro tipo de puntero y viceversa.
Un puntero puede ser null . La aplicación del operador de direccionamiento indirecto a un puntero NULL da
como resultado un comportamiento definido por la implementación.
Pasar punteros entre métodos puede provocar un comportamiento no definido. Valore la posibilidad de usar un
método que devuelva un puntero a una variable local mediante un parámetro in , out o ref , o bien como
resultado de la función. Si el puntero se estableció en un bloque fijo, es posible que la variable a la que señala ya
no sea fija.
En la tabla siguiente se muestran los operadores e instrucciones que pueden funcionar en punteros en un
contexto no seguro:
[] Indiza un puntero.
Para obtener más información sobre los operadores relacionados con el puntero, vea Operadores relacionados
con el puntero.
Cualquier tipo de puntero se puede convertir implícitamente en un tipo void* . Se puede asignar el valor null
a cualquier tipo de puntero. Cualquier tipo de puntero se puede convertir explícitamente en cualquier otro tipo
de puntero mediante una expresión de conversión. También puede convertir cualquier tipo entero en un tipo de
puntero o cualquier tipo de puntero en un tipo entero. Estas conversiones requieren una conversión explícita.
El siguiente ejemplo convierte un valor de tipo int* en byte* . Tenga en cuenta que el puntero señala al byte
dirigido más bajo de la variable. Cuando incrementa el resultado sucesivamente, hasta el tamaño de int (4
bytes), puede mostrar los bytes restantes de la variable.
unsafe
{
// Convert to byte:
byte* p = (byte*)&number;
/* Output:
The 4 bytes of the integer: 00 04 00 00
The value of the integer: 1024
*/
}
En el código seguro, un struct de C# que contiene una matriz no contiene los elementos de matriz. En su lugar, el
struct contiene una referencia a los elementos. Puede insertar una matriz de tamaño fijo en un struct cuando se
usa en un bloque de código no seguro.
El tamaño del siguiente struct no depende del número de elementos en la matriz, ya que pathName es una
referencia:
Un puede contener una matriz insertada en el código no seguro. En el siguiente ejemplo, la matriz
struct
fixedBuffer tiene un tamaño fijo. Se usa una instrucción fixed para establecer un puntero al primer elemento.
Se accede a los elementos de la matriz mediante este puntero. La instrucción fixed ancla el campo de instancia
fixedBuffer a una ubicación concreta en la memoria.
unsafe
{
// Pin the buffer to a fixed location in memory.
fixed (char* charPtr = example.buffer.fixedBuffer)
{
*charPtr = 'A';
}
// Access safely through the index:
char c = example.buffer.fixedBuffer[0];
Console.WriteLine(c);
El tamaño de la matriz char de 128 elementos es 256 bytes. Los búferes char de tamaño fijo siempre admiten
2 bytes por carácter, independientemente de la codificación. Este tamaño de matriz es el mismo, incluso cuando
se calculan las referencias de los búferes char a los métodos API o structs con CharSet = CharSet.Auto o
CharSet = CharSet.Ansi . Para obtener más información, vea CharSet.
En el ejemplo anterior se muestra cómo acceder a campos fixed sin anclar, lo que está disponible a partir de
C# 7.3.
Otra matriz de tamaño fijo común es la matriz bool. Los elementos de una matriz bool siempre tienen 1 byte
de tamaño. Las matrices bool no son adecuadas para crear matrices de bits o búferes.
Los búferes de tamaño fijo se compilan con el atributo
System.Runtime.CompilerServices.UnsafeValueTypeAttribute, que indica a Common Language Runtime (CLR)
que un tipo contiene una matriz no administrada que puede provocar un desbordamiento. La memoria
asignada mediante stackalloc también habilita automáticamente las características de detección de saturación
del búfer en CLR. En el ejemplo anterior se muestra cómo podría existir un búfer de tamaño fijo en un
unsafe struct .
[FixedBuffer(typeof(char), 128)]
public <fixedBuffer>e__FixedBuffer fixedBuffer;
}
Los búferes de tamaño fijo son diferentes de las matrices normales en los siguientes puntos:
Solo se pueden usar en un contexto unsafe .
Solo pueden ser campos de instancia de structs.
Siempre son vectores o matrices unidimensionales.
La declaración debe incluir la longitud, como fixed char id[8] . No puede usar fixed char id[] .
// If the number of bytes from the offset to the end of the array is
// less than the number of bytes you want to copy, you cannot complete
// the copy.
if ((source.Length - sourceOffset < count) ||
(target.Length - targetOffset < count))
{
throw new System.ArgumentException();
}
// The following fixed statement pins the location of the source and
// target objects in memory so that they will not be moved by garbage
// collection.
fixed (byte* pSource = source, pTarget = target)
{
// Copy the specified number of bytes from source to target.
for (int i = 0; i < count; i++)
{
pTarget[targetOffset + i] = pSource[sourceOffset + i];
}
}
}
Punteros de función
C# proporciona tipos delegate para definir objetos de puntero de función seguros. La invocación de un
delegado implica la creación de instancias de un tipo derivado de System.Delegate y la realización de una
llamada al método virtual a su método Invoke . Esta llamada virtual utiliza la instrucción de IL callvirt . En las
rutas de acceso de código crítico para el rendimiento, el uso de la instrucción de IL calli es más eficaz.
Puede definir un puntero de función mediante la sintaxis delegate* . El compilador llamará a la función
mediante la instrucción calli en lugar de crear una instancia de un objeto delegate y llamar a Invoke . En el
código siguiente se declaran dos métodos que usan delegate o delegate* para combinar dos objetos del
mismo tipo. El primer método usa un tipo de delegado System.Func<T1,T2,TResult>. El segundo método usa
una declaración delegate* con los mismos parámetros y el tipo de valor devuelto:
En el código siguiente se muestra cómo se declara una función local estática y se invoca el método
UnsafeCombine con un puntero a esa función local:
En el código anterior se muestran algunas de las reglas de la función a la que se accede como un puntero de
función:
Los punteros de función solo se pueden declarar en un contexto unsafe .
Los métodos que toman un tipo de delegate* (o devuelven un tipo de delegate* ) solo se pueden llamar en
un contexto unsafe .
Para obtener la dirección de una función, el operador & solo se permite en funciones static . Esta regla se
aplica a las funciones miembro y a las funciones locales.
La sintaxis muestra paralelismos con la declaración de tipos de delegate y el uso de punteros. El sufijo * de
delegate indica que la declaración es un puntero de función. El operador & , al asignar un grupo de métodos a
un puntero de función, indica que la operación toma la dirección del método.
Puede especificar la convención de llamada para un puntero delegate* mediante las palabras clave managed y
unmanaged . Además, en el caso de los punteros de función unmanaged , puede especificar la convención de
llamada. En las siguientes declaraciones, se muestran ejemplos de cada una. La primera declaración usa la
convención de llamada managed , que es la predeterminada. Las tres siguientes usan una convención de llamada
unmanaged . Cada una especifica una de las convenciones de llamada de ECMA 335: Cdecl , Stdcall , Fastcall
o Thiscall . Las últimas declaraciones usan la convención de llamada unmanaged , que indica al CLR que elija la
convención de llamada predeterminada para la plataforma. CLR elegirá la convención de llamada en tiempo de
ejecución.
Puede obtener más información sobre los punteros de función en la propuesta de Puntero de función para
C# 9.0.
Aunque el compilador no tiene un preprocesador independiente, las directivas descritas en esta sección se
procesan como si hubiera uno. Se usan para facilitar la compilación condicional. A diferencia de las directivas de
C y C++, estas no se pueden usar para crear macros. Una directiva de preprocesador debe ser la única
instrucción en una línea.
Compilación condicional
Para controlar la compilación condicional se usan cuatro directivas de preprocesador:
#if : abre una compilación condicional, donde el código solo se compila si se define el símbolo especificado.
#elif : cierra la compilación condicional anterior y abre una nueva en función de si se define el símbolo
especificado.
#else : cierra la compilación condicional anterior y abre una nueva si no se ha definido el símbolo
especificado anterior.
#endif : cierra la compilación condicional anterior.
Cuando el compilador de C# encuentra una directiva #if , seguida en última instancia por una directiva #endif
, compila el código entre las directivas solo si se ha definido el símbolo especificado. A diferencia de C y C++, no
se puede asignar un valor numérico a un símbolo. La instrucción #if en C# es booleana y solo comprueba si el
símbolo se ha definido o no. Por ejemplo:
#if DEBUG
Console.WriteLine("Debug version");
#endif
Puede usar los operadores == (igualdad) y != (desigualdad) para comprobar los valores bool true o false
. true significa que el símbolo está definido. La instrucción #if DEBUG tiene el mismo significado que
#if (DEBUG == true) . Puede usar los operadores && (y), || (o) y ! (no) para evaluar si se han definido varios
símbolos. Es posible agrupar símbolos y operadores mediante paréntesis.
#if , junto con las directivas #else , #elif , #endif , #define y #undef , permite incluir o excluir código
basado en la existencia de uno o varios símbolos. La compilación condicional puede resultar útil al compilar
código para una compilación de depuración o para una configuración concreta.
Una directiva condicional que empieza con una directiva #if debe terminar de forma explícita con una
directiva #endif . #define permite definir un símbolo. Al usar el símbolo como la expresión que se pasa a la
directiva #if , la expresión se evalúa como true . También se puede definir un símbolo con la opción del
compilador DefineConstants . La definición de un símbolo se puede anular mediante #undef . El ámbito de un
símbolo creado con #define es el archivo en que se ha definido. Un símbolo definido con DefineConstants o
#define no debe entrar en conflicto con una variable del mismo nombre. Es decir, un nombre de variable no se
debe pasar a una directiva de preprocesador y un símbolo solo puede ser evaluado por una directiva de
preprocesador.
#elif permite crear una directiva condicional compuesta. La expresión #elif se evaluará si ninguna de las
expresiones de directiva #if o #elif (opcional) precedentes se evalúan como true . Si una expresión #elif
se evalúa como true , el compilador evalúa todo el código comprendido entre #elif y la siguiente directiva
condicional. Por ejemplo:
#define VC7
//...
#if debug
Console.WriteLine("Debug build");
#elif VC7
Console.WriteLine("Visual Studio 7");
#endif
#else permite crear una directiva condicional compuesta, de modo que, si ninguna de las expresiones de las
directivas #if o #elif (opcional) anteriores se evalúan como true , el compilador evaluará todo el código
entre #else y la directiva #endif siguiente. #endif (#endif) debe ser la siguiente directiva de preprocesador
después de #else .
#endif especifica el final de una directiva condicional, que comienza con la directiva #if .
El sistema de compilación también tiene en cuenta los símbolos de preprocesador predefinidos que representan
distintos marcos de destino en proyectos de estilo SDK. Resultan útiles al crear aplicaciones que pueden tener
como destino más de una versión de .NET.
NOTE
En el caso de los proyectos que no son de estilo SDK, tendrá que configurar manualmente los símbolos de compilación
condicional para las diferentes plataformas de destino en Visual Studio a través de las páginas de propiedades del
proyecto.
Otros símbolos predefinidos incluyen las constantes DEBUG y TRACE . Puede invalidar los valores establecidos
para el proyecto con #define . Por ejemplo, el símbolo DEBUG se establece automáticamente según las
propiedades de configuración de compilación (modo de "depuración" o de "versión").
En el ejemplo siguiente se muestra cómo definir un símbolo MYTEST en un archivo y luego probar los valores de
los símbolos MYTEST y DEBUG . La salida de este ejemplo depende de si el proyecto se ha compilado en modo de
configuración Depuración o Versión .
#define MYTEST
using System;
public class MyClass
{
static void Main()
{
#if (DEBUG && !MYTEST)
Console.WriteLine("DEBUG is defined");
#elif (!DEBUG && MYTEST)
Console.WriteLine("MYTEST is defined");
#elif (DEBUG && MYTEST)
Console.WriteLine("DEBUG and MYTEST are defined");
#else
Console.WriteLine("DEBUG and MYTEST are not defined");
#endif
}
}
En el ejemplo siguiente se muestra cómo probar distintos marcos de destino para que se puedan usar las API
más recientes cuando sea posible:
public class MyClass
{
static void Main()
{
#if NET40
WebClient _client = new WebClient();
#else
HttpClient _client = new HttpClient();
#endif
}
//...
}
Definición de símbolos
Use las dos directivas de preprocesador siguientes para definir o anular la definición de símbolos para la
compilación condicional:
#define : se define un símbolo.
#undef : se anula la definición de un símbolo.
Usa #define para definir un símbolo. Si usa el símbolo como expresión que se pasa a la directiva #if , la
expresión se evaluará como true , como se muestra en el siguiente ejemplo:
#define VERBOSE
#if VERBOSE
Console.WriteLine("Verbose output version");
#endif
NOTE
La directiva #define no puede usarse para declarar valores constantes como suele hacerse en C y C++. En C#, las
constantes se definen mejor como miembros estáticos de una clase o struct. Si tiene varias constantes de este tipo, puede
considerar la posibilidad de crear una clase "Constants" independiente donde incluirlas.
Los símbolos se pueden usar para especificar condiciones de compilación. Puede comprobar el símbolo tanto
con #if como con #elif . También se puede usar ConditionalAttribute para realizar una compilación
condicional. Puede definir un símbolo, pero no asignar un valor a un símbolo. La directiva #define debe
aparecer en el archivo antes de que use cualquier instrucción que tampoco sea una directiva del preprocesador.
También se puede definir un símbolo con la opción del compilador DefineConstants . La definición de un
símbolo se puede anular mediante #undef .
Definición de regiones
Puede definir regiones de código que se pueden contraer en un esquema mediante las dos directivas de
preprocesador siguientes:
#region : se inicia una región.
#endregion : se finaliza una región.
#region permite especificar un bloque de código que se puede expandir o contraer cuando se usa la
característica de esquematización del editor de código. En archivos de código más largos, es conveniente
contraer u ocultar una o varias regiones para poder centrarse en la parte del archivo en la que se trabaja
actualmente. En el ejemplo siguiente se muestra cómo definir una región:
Un bloque #region se debe terminar con una directiva #endregion . Un bloque #region no se puede
superponer con un bloque #if . Pero, un bloque #region se puede anidar en un bloque #if y un bloque #if
se puede anidar en un bloque #region .
#error permite generar un error CS1029 definido por el usuario desde una ubicación específica en el código.
Por ejemplo:
NOTE
El compilador trata #error version de forma especial e informa de un error del compilador, CS8304, con un mensaje
que contiene las versiones que se usan del compilador y del lenguaje.
#warning permite generar una advertencia del compilador CS1030 de nivel uno desde una ubicación específica
en el código. Por ejemplo:
#line le permite modificar el número de línea del compilador y (opcionalmente) la salida del nombre de
archivo de errores y advertencias.
En el siguiente ejemplo, se muestra cómo notificar dos advertencias asociadas con números de línea. La
directiva #line 200 fuerza el número de línea siguiente para que sea 200 (aunque el valor predeterminado es
6) y hasta la siguiente directiva #line , el nombre de archivo se notificará como "Especial". La directiva
#line default devuelve la numeración de líneas a su numeración predeterminada, que cuenta las líneas a las
que la directiva anterior ha cambiado el número.
class MainClass
{
static void Main()
{
#line 200 "Special"
int i;
int j;
#line default
char c;
float f;
#line hidden // numbering not affected
string s;
double d;
}
}
Special(200,13): warning CS0168: The variable 'i' is declared but never used
Special(201,13): warning CS0168: The variable 'j' is declared but never used
MainClass.cs(9,14): warning CS0168: The variable 'c' is declared but never used
MainClass.cs(10,15): warning CS0168: The variable 'f' is declared but never used
MainClass.cs(12,16): warning CS0168: The variable 's' is declared but never used
MainClass.cs(13,16): warning CS0168: The variable 'd' is declared but never used
La directiva #line podría usarse en un paso intermedio automatizado en el proceso de compilación. Por
ejemplo, si se han eliminado las líneas del archivo de código fuente original, pero aún quiere que el compilador
genere unos resultados en función de la numeración de líneas original en el archivo, puede quitar las líneas y,
después, simular la numeración de líneas original con #line .
La directiva #line hidden oculta las líneas sucesivas del depurador, de forma que, cuando el desarrollador
ejecuta paso a paso el código, cualquier línea entre #line hidden y la siguiente directiva #line (siempre que
no sea otra directiva #line hidden ) se depurará paso a paso por procedimientos. Esta opción también se puede
usar para permitir que ASP.NET diferencie entre el código generado por el equipo y el definido por el usuario.
Aunque ASP.NET es el consumidor principal de esta característica, es probable que la usen más generadores de
código fuente.
Una directiva #line hidden no afecta a los nombres de archivo ni a los números de línea en el informe de
errores. Es decir, si el compilador detecta un error en un bloque oculto, notificará el nombre de archivo y
número de línea actuales del error.
La directiva #line filename especifica el nombre de archivo que quiere que aparezca en la salida del
compilador. De forma predeterminada, se usa el nombre real del archivo de código fuente. El nombre de archivo
debe estar entre comillas dobles ("") y debe ir precedido de un número de línea.
En el ejemplo siguiente, se muestra cómo el depurador omite las líneas ocultas en el código. Al ejecutar el
ejemplo, mostrará tres líneas de texto. Pero al establecer un punto de interrupción, como se muestra en el
ejemplo, y presionar F10 para ejecutar el código paso a paso, el depurador omite la línea oculta. Incluso si
establece un punto de interrupción en la línea oculta, el depurador la omitirá.
// preprocessor_linehidden.cs
using System;
class MainClass
{
static void Main()
{
Console.WriteLine("Normal line #1."); // Set break point here.
#line hidden
Console.WriteLine("Hidden line.");
#line default
Console.WriteLine("Normal line #2.");
}
}
Pragmas
#pragma proporciona al compilador instrucciones especiales para la compilación del archivo en el que aparece.
Las instrucciones deben ser compatibles con el compilador. Es decir, no puede usar #pragma para crear
instrucciones de preprocesamiento personalizadas.
#pragma warning : se habilitan o deshabilitan las advertencias.
#pragma checksum : se genera una lista de comprobación.
Donde warning-list es una lista de números de advertencia separados por comas. El prefijo "CS" es opcional.
Cuando no se especifica ningún número de advertencia, disable deshabilita todas las advertencias y restore
habilita todas las advertencias.
NOTE
Para buscar los números de advertencia en Visual Studio, compile el proyecto y después busque los números de
advertencia en la ventana Salida .
disable surte efecto a partir de la siguiente línea del archivo de código fuente. La advertencia se restaura en la
línea que sigue a restore . Si el archivo no incluye restore , las advertencias se restauran a su estado
predeterminado en la primera línea de los archivos posteriores de la misma compilación.
// pragma_warning.cs
using System;
Donde "filename" es el nombre del archivo en el que es necesario supervisar los cambios o las actualizaciones,
"{guid}" es el identificador único global (GUID) para el algoritmo hash y "checksum_bytes" es la cadena de
dígitos hexadecimales que representa los bytes de la suma de comprobación. Debe ser un número par de
dígitos hexadecimales. Un número impar de dígitos genera una advertencia de tiempo de compilación y la
directiva se ignora.
El depurador de Visual Studio usa una suma de comprobación para asegurarse de que siempre encuentra el
código fuente correcto. El compilador calcula la suma de comprobación para un archivo de origen y, después,
emite el resultado en el archivo de base de datos del programa (PDB). Después, el depurador usa el archivo PDB
para comparar la suma de comprobación que calcula para el archivo de origen.
Esta solución no funciona con proyectos de ASP.NET, ya que la suma de comprobación calculada es para el
archivo de código fuente generado, no para el archivo .aspx. Para solucionar este problema, #pragma checksum
proporciona compatibilidad de la suma de comprobación con páginas ASP.NET.
Cuando se crea un proyecto de ASP.NET en Visual C#, el archivo de código fuente generado contiene una suma
de comprobación del archivo .aspx desde el que se genera el código fuente. Después, el compilador escribe esta
información en el archivo PDB.
Si el compilador no encuentra ninguna directiva #pragma checksum en el archivo, calcula la suma de
comprobación y escribe el valor en el archivo PDB.
class TestClass
{
static int Main()
{
#pragma checksum "file.cs" "{406EA660-64CF-4C82-B6F0-42D48172A799}" "ab007f1d23d9" // New checksum
}
}
Opciones del compilador de C#
16/09/2021 • 2 minutes to read
En esta sección se describen las opciones que interpreta el compilador de C#. Las opciones se agrupan en
artículos independientes en función de lo que controlan, por ejemplo, las características del lenguaje, la
generación de código y la salida. Use la tabla de contenido para navegar por ellas.
<PropertyGroup>
<LangVersion>preview</LangVersion>
</PropertyGroup>
Para obtener más información sobre el procedimiento para establecer las opciones en los archivos de
proyecto, consulte el artículo Referencia de MSBuild para proyectos del SDK de .NET.
Uso de las páginas de propiedades de Visual Studio
Visual Studio cuenta con páginas de propiedades para editar las propiedades de compilación. Para
obtener más información al respecto, consulte Administrar propiedades de soluciones y proyectos
(Windows) y Administrar propiedades de soluciones y proyectos (Mac).
Proyectos de .NET Framework
IMPORTANT
Esta sección solo se aplica a los proyectos de .NET Framework.
Además de los mecanismos descritos anteriormente, puede establecer las opciones del compilador por medio
de dos métodos más para los proyectos de .NET Framework:
Argumentos de la línea de comandos para proyectos de .NET Framework : los proyectos de .NET
Framework usan csc.exe en lugar de dotnet build para compilar los proyectos. En el caso de los proyectos
de .NET Framework, puede especificar argumentos de la línea de comandos a csc.exe.
Páginas de ASP.NET compiladas : los proyectos de .NET Framework usan una sección del archivo
web.config para compilar páginas. Para el nuevo sistema de compilación, así como los proyectos de ASP.NET
Core, las opciones se toman del archivo del proyecto.
En el caso de algunas opciones del compilador, la palabra ha cambiado de csc.exe al nuevo sistema de MSBuild,
y también para los proyectos de .NET Framework. En esta sección se emplea la nueva sintaxis. Ambas versiones
figuran al principio de cada página. Para csc.exe, los argumentos figuran seguidos de la opción y dos puntos. Por
ejemplo, la opción -doc sería:
-doc:DocFile.xml
Puede invocar el compilador de C# escribiendo el nombre de su archivo ejecutable (csc.exe) en un símbolo del
sistema.
En el caso de los proyectos de .NET Framework, también puede ejecutar csc.exe mediante la línea de comandos.
Cada opción del compilador está disponible en dos formatos: -option y /option . En los proyectos web de .NET
Framework, debe especificar las opciones para compilar el código subyacente en el archivo web.config. Para
obtener más información, consulte Elemento <compiler>.
Si usa la ventana Símbolo del sistema para desarrolladores de Visual Studio , todas las variables de
entorno necesarias se establecen automáticamente. Para obtener información sobre cómo acceder a esta
herramienta, vea Símbolo del sistema para desarrolladores de Visual Studio.
El archivo ejecutable csc.exe normalmente se encuentra en la carpeta Microsoft.NET\Framework\ <Version> , en
el directorio Windows. Su ubicación puede variar, según la configuración exacta de un equipo concreto. Si se
instala más de una versión de .NET Framework en el equipo, encontrará varias versiones de este archivo. Para
obtener más información sobre estas instalaciones, vea Cómo: Determinar qué versiones de .NET Framework
están instaladas.
Opciones del compilador de C# para las reglas de
características del lenguaje
16/09/2021 • 8 minutes to read
Las opciones siguientes controlan cómo el compilador interpreta las características del lenguaje. La nueva
sintaxis de MSBuild se muestra en negrita . La sintaxis de csc.exe anterior se muestra en code style .
CheckForOverflowUnderflow / -checked : se generan comprobaciones de desbordamiento.
AllowUnsafeBlocks / -unsafe : se permite código "no seguro".
DefineConstants / -define : se definen símbolos de compilación condicional.
LangVersion / -langversion : se especifica la versión del lenguaje como default (versión principal más
reciente) o latest (versión más reciente, incluidas las versiones secundarias).
Nullable / -nullable : se habilita el contexto que admite un valor NULL o advertencias que admiten un
valor NULL.
CheckForOverflowUnderflow
La opción CheckForOverflowUnderflow especifica si una instrucción aritmética de enteros que produce un
valor fuera del intervalo del tipo de datos genera una excepción en tiempo de ejecución.
<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
Una instrucción aritmética de enteros que está en el ámbito de una palabra clave checked o unchecked no está
sujeta al efecto de la opción CheckForOverflowUnderflow . Si una instrucción aritmética de enteros que no
está en el ámbito de una palabra clave checked o unchecked produce un valor fuera del intervalo del tipo de
datos, y CheckForOverflowUnderflow es true , esa instrucción inicia una excepción en tiempo de ejecución.
Si CheckForOverflowUnderflow es false , esa instrucción inicia una excepción en tiempo de ejecución. El
valor predeterminado para esta opción es false y la comprobación de desbordamiento está deshabilitada.
AllowUnsafeBlocks
La opción del compilador AllowUnsafeBlocks permite la compilación de código en el que se usa la palabra
clave unsafe. El valor predeterminado de esta opción es false , lo que significa que no se permite el código no
seguro.
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
Para obtener más información sobre el código no seguro, vea Código no seguro y punteros.
DefineConstants
La opción DefineConstants define símbolos en todos los archivos de código fuente del programa.
<DefineConstants>name;name2</DefineConstants>
Esta opción especifica los nombres de uno o más símbolos que quiera definir. La opción DefineConstants tiene
el mismo efecto que la directiva del preprocesador #define, salvo que la opción del compilador está en vigor
para todos los archivos del proyecto. Un símbolo permanece definido en un archivo de origen hasta que una
directiva #undef en el archivo de origen quita la definición. Cuando usa la opción -define , una directiva
#undef en un archivo no tiene ningún efecto en otros archivos de código fuente del proyecto. Los símbolos
creados por esta opción se pueden usar con #if, #else, #elif y #endif para compilar los archivos de origen
condicionalmente. El propio compilador de C# no define ningún símbolo o macro que puede usar en su código
fuente; todas las definiciones de símbolo deben definirse por el usuario.
NOTE
El valor de la directiva #define de C# no permite que se le proporcione un valor a un símbolo, como sucede en
lenguajes como C++. Por ejemplo, #define no puede usarse para crear una macro o para definir una constante. Si
necesita definir una constante, use una variable enum . Si quiere crear una macro de estilo de C++, considere alternativas
como genéricos. Como las macros son notoriamente propensas a errores, C# deshabilita su uso pero proporciona
alternativas más seguras.
LangVersion
Hace que el compilador acepte solo la sintaxis que se incluye en la especificación elegida del lenguaje C#.
<LangVersion>9.0</LangVersion>
VA LO R SIGN IF IC A DO
La versión de idioma predeterminada depende de la plataforma de destino de la aplicación y la versión del SDK
o de Visual Studio instalada. Esas reglas se definen en el control de versiones del lenguaje C#.
Los metadatos a los que hace referencia la aplicación de C# no están sujetos a la opción del compilador
LangVersion .
Como cada versión del compilador de C# contiene extensiones para la especificación del lenguaje,
LangVersion no ofrece las funciones equivalentes de una versión anterior del compilador.
Además, aunque las actualizaciones de versión de C# generalmente coinciden con las versiones principales de
.NET Framework, la sintaxis y las características nuevas no están necesariamente asociadas a esa versión de
marco específica. Aunque las nuevas características necesitan definitivamente una nueva actualización del
compilador que también se publica junto con la revisión de C#, cada característica específica tiene su propia API
mínima de .NET o requisitos de Common Language Runtime que pueden permitir que se ejecute en marcos de
versiones anteriores mediante la inclusión de paquetes NuGet u otras bibliotecas.
Independientemente de la configuración de LangVersion que utilice, use la versión actual de Common
Language Runtime para crear el archivo .exe o .dll. Una excepción son los ensamblados de confianza y
ModuleAssemblyName , que funcionan en -langversion:ISO-1 .
Para obtener otras formas de especificar la versión del lenguaje C#, vea Control de versiones del lenguaje C#.
Para obtener información sobre cómo establecer esta opción del compilador mediante programación, vea
LanguageVersion.
Especificación del lenguaje C#
VERSIÓ N VÍN C ULO DESC RIP C IÓ N
Versión del SDK mínima necesaria para admitir todas las características del lenguaje
En la tabla siguiente se enumeran las versiones mínimas del SDK con el compilador de C# que admite la versión
del lenguaje correspondiente:
Nullable
La opción Nullable le permite especificar el contexto que admite un valor NULL.
<Nullable>enable</Nullable>
El argumento debe ser uno de enable , disable , warnings o annotations . El argumento enable habilita el
contexto que admite un valor NULL. Si se especifica disable , se deshabilitará el contexto que admite un valor
NULL. Al proporcionar el argumento warnings , se habilita el contexto de advertencia que admite un valor NULL.
Al especificar el argumento annotations , se habilita el contexto de anotación que admite un valor NULL.
El análisis de flujo se utiliza para inferir la nulabilidad de las variables del código ejecutable. La nulabilidad
inferida de una variable es independiente de la nulabilidad declarada de dicha variable. Las llamadas de método
se analizan incluso cuando se omiten de forma condicional. Es el caso de Debug.Assert en modo de versión.
La invocación de métodos anotados con los siguientes atributos también afectará al análisis de flujo:
Condiciones previas simples: AllowNullAttribute y DisallowNullAttribute
Condiciones posteriores simples: MaybeNullAttribute y NotNullAttribute
Condiciones posteriores condicionales: MaybeNullWhenAttribute y NotNullWhenAttribute
DoesNotReturnIfAttribute (por ejemplo, DoesNotReturnIf(false) para Debug.Assert) y
DoesNotReturnAttribute
NotNullIfNotNullAttribute
Condiciones posteriores de miembros: MemberNotNullAttribute(String) y MemberNotNullAttribute(String[])
IMPORTANT
El contexto global que admite un valor NULL no se aplica a los archivos de código generado. Con independencia de este
valor, el contexto que admite un valor NULL está deshabilitado para cualquier archivo de código fuente marcado como
generado. Hay cuatro maneras de marcar un archivo como generado:
1. En el archivo .editorconfig, especifique generated_code = true en una sección que se aplique a ese archivo.
2. Coloque <auto-generated> o <auto-generated/> en un comentario en la parte superior del archivo. Puede estar
en cualquier línea de ese comentario, pero el bloque de comentario debe ser el primer elemento del archivo.
3. Inicie el nombre de archivo con TemporaryGeneratedFile_
4. Finalice el nombre de archivo con .designer.cs, .generated.cs, .g.cs o .g.i.cs.
Los generadores pueden optar por usar la directiva de preprocesador #nullable .
Opciones del compilador de C# que controlan la
salida del compilador
16/09/2021 • 9 minutes to read
Las opciones siguientes controlan la generación de salida del compilador. La nueva sintaxis de MSBuild se
muestra en negrita . La sintaxis de csc.exe anterior se muestra en code style .
DocumentationFile / -doc : se genera un archivo de documentación XML a partir de los comentarios de
/// .
OutputAssembly / -out : se especifica el archivo de ensamblado de salida.
PlatformTarget / -platform : se especifica la CPU de la plataforma de destino.
ProduceReferenceAssembly / -refout : se genera un ensamblado de referencia.
TargetType -target : se especifica el tipo del ensamblado de salida.
DocumentationFile
La opción DocumentationFile permite insertar comentarios de documentación en un archivo XML. Para
obtener más información sobre cómo documentar el código, vea Etiquetas recomendadas para comentarios de
documentación. El valor especifica la ruta al archivo XML de salida. El archivo XML contiene los comentarios en
los archivos de código fuente de la compilación.
<DocumentationFile>path/to/file.xml</DocumentationFile>
El archivo de código fuente que contiene instrucciones principales o de nivel superior se genera primero en el
XML. A menudo, querrá usar el archivo .xml generado con IntelliSense. El nombre de archivo .xml debe ser el
mismo que el nombre del ensamblado. El archivo .xml debe estar en el mismo directorio que el ensamblado.
Cuando se hace referencia al ensamblado en un proyecto de Visual Studio, también se encuentra el archivo .xml.
Para obtener más información sobre la generación de comentarios de código, vea Proporcionar comentarios de
código. A menos que realice la compilación con <TargetType:Module> , file contendrá etiquetas <assembly> y
</assembly> que especifican el nombre del archivo que incluye el manifiesto del ensamblado para el archivo de
salida. Para obtener ejemplos, vea Procedimiento para usar las características de la documentación XML.
NOTE
La opción DocumentationFile se aplica a todos los archivos del proyecto. Para deshabilitar las advertencias relacionadas
con los comentarios de documentación para un archivo específico o una sección de código, use #pragma warning.
OutputAssembly
La opción OutputAssembly especifica el nombre del archivo de salida. En la ruta de salida se especifica la
carpeta donde se coloca la salida del compilador.
<OutputAssembly>folder</OutputAssembly>
Hay que especificar el nombre completo y la extensión del archivo que se quiere crear. Si no especifica el
nombre del archivo de salida, MSBuild usa el nombre del proyecto para especificar el nombre del ensamblado
de salida. Los proyectos de estilo antiguo usan las reglas siguientes:
Un archivo .exe toma el nombre del archivo de código fuente que contiene el método Main o instrucciones
de nivel superior.
Un archivo .dll o .netmodule toma el nombre del primer archivo de código fuente.
Todos los módulos que se produzcan como parte de una compilación se convierten en archivos asociados a
cualquier ensamblado que también se haya producido en la compilación. Use ildasm.exe para ver el manifiesto
del ensamblado y los archivos asociados.
Es obligatorio usar la opción del compilador OutputAssembly para que un archivo exe sea el destino de un
ensamblado de confianza.
PlatformTarget
Especifica qué versión de CLR puede ejecutar el ensamblado.
<PlatformTarget>anycpu</PlatformTarget>
anycpu (valor predeterminado) compila el ensamblado de forma que se pueda ejecutar en cualquier
plataforma. La aplicación se ejecuta como un proceso de 64 bits siempre que sea posible y recurre a 32 bits
solo cuando el modo está disponible.
anycpu32bitpreferred compila el ensamblado de forma que se pueda ejecutar en cualquier plataforma. La
aplicación se ejecuta en modo de 32 bits en sistemas que admiten aplicaciones de 64 y 32 bits. Solo puede
especificar esta opción para los proyectos que tienen como destino .NET Framework 4.5 o versiones
posteriores.
ARM compila el ensamblado de forma que pueda ejecutarse en un equipo que tenga un procesador
Advanced RISC Machine (ARM).
ARM64 compila el ensamblado que se va a ejecutar mediante el CLR de 64 bits en un equipo que tiene un
procesador Advanced RISC Machine (ARM) que admite el conjunto de instrucciones A64.
x64 compila el ensamblado de forma que el CLR de 64 bits pueda ejecutarlo en equipos compatibles con el
conjunto de instrucciones AMD64 o EM64T.
x86 compila el ensamblado de forma que el CLR de 32 bits compatible con x86 pueda ejecutarlo.
Itanium compila el ensamblado de forma que el CLR de 64 bits pueda ejecutarlo en un equipo con un
procesador Itanium.
En un sistema operativo de Windows de 64 bits:
Los ensamblados compilados con x86 se ejecutan en el CLR de 32 bits que se ejecuta en WOW64.
Un archivo DLL compilado con anycpu se ejecuta en el mismo CLR que el proceso en el que se ha cargado.
Los archivos ejecutables que se compilan con anycpu se ejecutan en el CLR de 64 bits.
Los archivos ejecutables compilados con anycpu32bitpreferred se ejecutan en el CLR de 32 bits.
El valor anycpu32bitpreferred solo es válido para archivos ejecutables (.EXE) y requiere .NET Framework 4.5 o
una versión posterior. Para obtener más información sobre cómo desarrollar una aplicación para que se ejecute
en un sistema operativo Windows de 64 bits, consulte Aplicaciones de 64 bits.
La opción PlatformTarget se establece en la página de propiedades de compilación del proyecto en
Visual Studio.
El comportamiento de anycpu tiene algunos matices adicionales en .NET Core y .NET 5 y versiones posteriores.
Cuando establezca anycpu , publique la aplicación y ejecútela con dotnet.exe de x86 o dotnet.exe de x64. En
el caso de las aplicaciones independientes, en el paso dotnet publish se empaqueta el archivo ejecutable para
el RID de configuración.
ProduceReferenceAssembly
La opción ProduceReferenceAssembly especifica una ruta de archivo donde se debe mostrar el ensamblado
de referencia. Se traduce por metadataPeStream en la API de emisión. filepath especifica la ruta para el
ensamblado de referencia. Generalmente debe coincidir con el ensamblado principal. La convención
recomendada (que usa MSBuild) consiste en colocar el ensamblado de referencia en una subcarpeta "ref/" con
relación al ensamblado principal.
<ProduceReferenceAssembly>filepath</ProduceReferenceAssembly>
Los ensamblados de referencia son un tipo especial de ensamblado que contiene solo la cantidad mínima de
metadatos necesarios para representar la superficie de API pública de la biblioteca. Incluyen declaraciones para
todos los miembros importantes al hacer referencia a un ensamblado en las herramientas de compilación. Los
ensamblados de referencia excluyen todas las implementaciones de miembros y declaraciones de miembros
privados. Esos miembros no tienen ningún impacto observable en su contrato de API. Para obtener más
información, vea Ensamblados de referencia en la Guía de .NET.
Las opciones ProduceReferenceAssembly y ProduceOnlyReferenceAssembly son mutuamente
excluyentes.
TargetType
La opción del compilador TargetType se puede especificar en uno de los formatos siguientes:
librar y : para crear una biblioteca de código. librar y es el valor predeterminado.
exe : para crear un archivo .exe.
module para crear un módulo.
winexe para crear un programa de Windows.
winmdobj para crear un archivo .winmdobj intermedio.
appcontainerexe para crear un archivo .exe para aplicaciones Windows 8.x de Microsoft Store.
NOTE
Para los destinos de .NET Framework, a menos que especifique module , esta opción hace que se coloque un manifiesto
del ensamblado de .NET Framework en un archivo de salida. Para obtener más información, vea Ensamblados en .NET y
Atributos comunes.
<TargetType>library</TargetType>
El compilador crea solo un manifiesto de ensamblado por compilación. La información sobre todos los archivos
de una compilación se coloca en el manifiesto de ensamblado. Cuando se generan varios archivos de salida en
la línea de comandos, solo se puede crear un manifiesto de ensamblado que debe ir en el primer archivo de
salida especificado en la línea de comandos.
Si crea un ensamblado, puede indicar que todo o parte del código es conforme a CLS mediante el atributo
CLSCompliantAttribute.
biblioteca
La opción librar y hace que el compilador cree una biblioteca de vínculos dinámicos (DLL) en lugar de un
archivo ejecutable (EXE). El archivo DLL se creará con la extensión .dll. A menos que se especifique lo contrario
con la opción OutputAssembly , el archivo de salida adopta el nombre del primer archivo de entrada. Al
compilar un archivo .dll, no es necesario un método Main .
exe
La opción exe hace que el compilador cree una aplicación de consola ejecutable (EXE). El archivo ejecutable se
creará con la extensión .exe. Use winexe para crear un archivo ejecutable de un programa de Windows. A
menos que se especifique de otro modo con la opción OutputAssembly , el nombre del archivo de salida toma
el del archivo de entrada que contiene el punto de entrada (el método Main o instrucciones de nivel superior).
Solo se necesita un punto de entrada en los archivos de código fuente que se compilan en un archivo .exe. La
opción del compilador Star tupObject le permite especificar qué clase contiene el método Main , en aquellos
casos en los que el código tenga más de una clase con un método Main .
module
Esta opción hace que el compilador no genere un manifiesto del ensamblado. De forma predeterminada, el
archivo de salida creado al realizar la compilación con esta opción tendrá una extensión .netmodule. El entorno
de ejecución de .NET no puede cargar un archivo que no tiene un manifiesto del ensamblado. Pero este archivo
se puede incorporar en el manifiesto del ensamblado con AddModules . Si se crea más de un módulo en una
única compilación, los tipos internal que haya en un módulo estarán disponibles para otros módulos de la
compilación. Cuando el código de un módulo hace referencia a tipos internal de otro, se deben incorporar los
dos módulos en un manifiesto del ensamblado, mediante AddModules . No se admite la creación de un
módulo en el entorno de desarrollo de Visual Studio.
winexe
La opción winexe hace que el compilador cree un programa de Windows ejecutable (EXE). El archivo ejecutable
se creará con la extensión .exe. Un programa de Windows es el que ofrece una interfaz de usuario de la
biblioteca de .NET o con las API Windows. Use exe para crear una aplicación de consola. A menos que se
especifique de otro modo con la opción OutputAssembly , el nombre del archivo de salida toma el nombre del
archivo de entrada que contiene el método Main . Solo se necesita un método Main en los archivos de código
fuente que se compilan en un archivo .exe. La opción Star tupObject le permite especificar qué clase contiene el
método Main , en aquellos casos en los que el código tenga más de una clase con un método Main .
winmdobj
Si usa la opción winmdobj , el compilador crea un archivo .winmdobj intermedio que se puede convertir en un
archivo binario de Windows Runtime ( .winmd). Después, el archivo .winmd se puede usar en programas de
JavaScript y C++, además de programas de lenguajes administrados.
El valor winmdobj indica al compilador que un módulo intermedio es obligatorio. Después, el archivo
.winmdobj se puede proporcionar por medio de la herramienta de exportación WinMDExp para generar un
archivo de metadatos de Windows ( .winmd). El archivo .winmd contiene el código de la biblioteca original y los
metadatos de WinMD que usan JavaScript o C++, y Windows Runtime. La salida de un archivo compilado
mediante la opción del compilador winmdobj solo se usa como entrada de la herramienta de exportación
WimMDExp. No se hace referencia directamente al archivo .winmdobj. A menos que use la opción
OutputAssembly , el nombre del archivo de salida tomar el del primer archivo de entrada. No se necesita un
método Main .
appcontainerexe
Si usa la opción del compilador appcontainerexe , este crea un archivo ejecutable de Windows ( .exe) que se
debe ejecutar en un contenedor de la aplicación. Esta opción equivale a -target:winexe, pero está diseñada para
las aplicaciones de la Tienda Windows 8.x.
Para exigir que la aplicación se ejecute en un contenedor de la aplicación, esta opción establece un bit en el
archivo portable ejecutable (PE). Cuando se establece ese bit, se produce un error si el método CreateProcess
intenta iniciar el archivo ejecutable fuera de un contenedor de la aplicación. A menos que use la opción
OutputAssembly , el nombre del archivo de salida toma el del archivo de entrada que contiene el método
Main .
Opciones del compilador de C# que especifican
entradas
16/09/2021 • 5 minutes to read
Las opciones siguientes controlan las entradas del compilador. La nueva sintaxis de MSBuild se muestra en
negrita . La sintaxis de csc.exe anterior se muestra en code style .
References / -reference o -references : se hace referencia a los metadatos de los archivos de ensamblado
especificados.
AddModules / -addmodule : se agrega un módulo (creado con target:module para este ensamblado).
EmbedInteropTypes / -link : se insertan metadatos de los archivos de ensamblado de interoperabilidad
especificados.
Referencias
La opción References hace que el compilador importe información de tipo public del archivo especificado al
proyecto actual, lo que permite hacer referencia a metadatos de los archivos de ensamblado especificados.
filename es el nombre de un archivo que contiene un manifiesto del ensamblado. Para importar más de un
archivo, incluya un elemento Reference diferente para cada archivo. Puede definir un alias como un elemento
secundario del elemento Reference :
<Reference Include="filename.dll">
<Aliases>LS</Aliases>
</Reference>
En el ejemplo anterior, LS es el identificador de C# válido que representa un espacio de nombres raíz que
contendrá todos los espacios de nombres en el archivo filename.dll del ensamblado. Los archivos que importe
deben contener un manifiesto. Use AdditionalLibPaths para especificar el directorio en el que se encuentran
una o varias de las referencias de ensamblado. En el tema AdditionalLibPaths también se describen los
directorios en los que el compilador busca ensamblados. Para que el compilador reconozca un tipo de un
ensamblado (y no de un módulo), debe obligársele a que resuelva el tipo, lo que se puede conseguir si se define
una instancia del tipo. Existen otras formas de que el compilador resuelva nombres de tipos en un ensamblado;
por ejemplo, si se hereda de un tipo de un ensamblado, el compilador reconocerá el nombre del tipo. A veces es
necesario hacer referencia a dos versiones diferentes del mismo componente desde un ensamblado. Para ello,
use el elemento Aliases del elemento References de cada archivo para distinguir entre los dos archivos. Este
alias se usará como calificador para el nombre del componente y se resolverá en el componente de uno de los
archivos.
NOTE
En Visual Studio, use el comando Agregar referencia . Para obtener más información, consulta Procedimiento para
agregar o quitar referencias mediante el Administrador de referencias.
AddModules
Esta opción agrega un módulo que se ha creado con el modificador <TargetType>module</TargetType> para la
compilación actual:
Donde file , file2 son archivos de salida que contienen metadatos. El archivo no puede contener un
manifiesto del ensamblado. Para importar más de un archivo, hay que separar los nombres de archivo con una
coma o un punto y coma. Todos los módulos agregados con AddModules deben encontrarse en el mismo
directorio que el archivo de salida en tiempo de ejecución. Es decir, puede especificar un módulo de cualquier
directorio en el momento de la compilación, pero el módulo debe encontrarse en el directorio de la aplicación
en tiempo de ejecución. Si el módulo no se encuentra en el directorio de la aplicación en tiempo de ejecución,
obtendrá TypeLoadException. file no puede contener ningún ensamblado. Por ejemplo, si el archivo de salida
se ha creado con la opción TargetType de module , sus metadatos se pueden importar con AddModules .
Si el archivo de salida se ha creado con una opción TargetType diferente de module , no se podrán importar
sus metadatos con AddModules , pero sí con References .
EmbedInteropTypes
Hace que el compilador facilite al proyecto que se está compilando información de tipos COM en los
ensamblados especificados.
<References>
<EmbedInteropTypes>file1;file2;file3</EmbedInteropTypes>
</References>
Donde file1;file2;file3 es una lista de nombres de archivo de ensamblado delimitados por signos de punto y
coma. Si el nombre de archivo contiene un espacio, escríbalo entre comillas. La opción EmbedInteropTypes
permite implementar una aplicación que tiene información de tipo insertada. La aplicación puede usar los tipos
de un ensamblado en tiempo de ejecución que implementan la información de tipo incrustada sin necesidad de
una referencia al ensamblado en tiempo de ejecución. Si hay varias versiones del ensamblado en tiempo de
ejecución publicadas, la aplicación que contiene la información de tipo incrustada puede trabajar con las
distintas versiones sin tener que volver a compilar. Para obtener un ejemplo, vea Tutorial: Insertar los tipos de los
ensamblados administrados.
La opción EmbedInteropTypes resulta de especial utilidad cuando se trabaja con la interoperabilidad COM.
Puede incrustar tipos COM para que la aplicación ya no necesite un ensamblado de interoperabilidad primario
(PIA) en el equipo de destino. La opción EmbedInteropTypes indica al compilador que inserte la información
de tipo COM del ensamblado de interoperabilidad al que se hace referencia en el código compilado resultante.
El tipo COM se identifica mediante el valor de CLSID (GUID). Como resultado, la aplicación se puede ejecutar en
un equipo de destino que tenga instalados los mismos tipos COM con los mismos valores de CLSID. Las
aplicaciones que automatizan Microsoft Office son un buen ejemplo. Dado que las aplicaciones como Office
suelen mantener el mismo valor de CLSID en las distintas versiones, la aplicación puede usar los tipos COM a
los que se hace referencia siempre que .NET Framework 4 o posterior esté instalado en el equipo de destino y la
aplicación emplee métodos, propiedades o eventos que estén incluidos en los tipos COM a los que se hace
referencia. La opción EmbedInteropTypes solo inserta interfaces, estructuras y delegados. No se admite la
inserción de clases COM.
NOTE
Cuando se crea una instancia de un tipo COM incrustado en el código, hay que crear la instancia mediante la interfaz
adecuada. Si se intenta crear una instancia de un tipo COM incrustado mediante la coclase, se produce un error.
Como sucede con la opción del compilador References , la opción del compilador EmbedInteropTypes usa el
archivo de respuesta Csc.rsp, que hace referencia a ensamblados de .NET usados con frecuencia. Use la opción
del compilador NoConfig si no quiere que el compilador utilice el archivo Csc.rsp.
Los tipos que tienen un parámetro genérico cuyo tipo se ha incrustado desde un ensamblado de
interoperabilidad no se pueden usar si ese tipo pertenece a un ensamblado externo. Esta restricción no se aplica
a las interfaces. Por ejemplo, considere la interfaz Range que se define en el ensamblado
Microsoft.Office.Interop.Excel. Si una biblioteca inserta tipos de interoperabilidad desde el ensamblado
Microsoft.Office.Interop.Excel y expone un método que devuelve un tipo genérico que tiene un parámetro cuyo
tipo es la interfaz Range, ese método debe devolver una interfaz genérica, como se muestra en el ejemplo de
código siguiente.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Office.Interop.Excel;
En el ejemplo siguiente, el código de cliente puede llamar al método que devuelve la interfaz genérica IList sin
errores.
Las opciones siguientes controlan cómo el compilador notifica los errores y las advertencias. La nueva sintaxis
de MSBuild se muestra en negrita . La sintaxis de csc.exe anterior se muestra en code style .
WarningLevel / -warn : se establece el nivel de advertencia.
TreatWarningsAsErrors / -warnaserror : todas las advertencias se tratan como errores.
WarningsAsErrors / -warnaserror : una o varias advertencias se tratan como errores
WarningsNotAsErrors / -warnnotaserror : una o varias advertencias no se tratan como errores
DisabledWarnings / -nowarn : se establece una lista de advertencias deshabilitadas.
CodeAnalysisRuleSet / -ruleset : se especifica un archivo de conjunto de reglas que deshabilita
diagnósticos específicos.
ErrorLog / -errorlog : se especifica un archivo para registrar todos los diagnósticos del compilador y el
analizador.
Repor tAnalyzer / -reportanalyzer : se notifica información adicional del analizador, como el tiempo de
ejecución.
WarningLevel
La opción WarningLevel especifica el nivel de advertencia que debe mostrar el compilador.
<WarningLevel>3</WarningLevel>
El valor del elemento es el nivel de advertencia que quiere que se muestre para la compilación: los números
más bajos muestran solo advertencias de gravedad alta. Los números más altos muestran más advertencias. El
valor debe ser cero o un entero positivo:
Para obtener información sobre un error o advertencia, puede buscar el código de error en el Índice de la Ayuda.
Para conocer otras maneras de obtener información sobre un error o advertencia, vea Errores del compilador de
C#. Use TreatWarningsAsErrors para tratar todas las advertencias como errores. Use DisabledWarnings
para deshabilitar advertencias concretas.
TreatWarningsAsErrors
La opción TreatWarningsAsErrors trata todas las advertencias como errores. También puede usar
TreatWarningsAsErrors para establecer solo algunas advertencias como errores. Si activa
TreatWarningsAsErrors , puede usar TreatWarningsAsErrors para enumerar las advertencias que no se
deben tratar como errores.
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
En su lugar, todos los mensajes de advertencia se notifican como errores. El proceso de compilación se detiene
(no se genera ningún archivo de salida). De forma predeterminada, TreatWarningsAsErrors no está en efecto,
lo que significa que las advertencias no impedirán la generación de un archivo de salida. Opcionalmente, si solo
quiere que algunas advertencias específicas se traten como errores, puede especificar una lista separada por
comas de números de advertencia que se tratarán como errores. Se puede especificar el conjunto de todas las
advertencias de nulabilidad con la abreviatura Nullable . Use WarningLevel para especificar el nivel de
advertencias que quiere que muestre el compilador. Use DisabledWarnings para deshabilitar advertencias
concretas.
WarningsAsErrors y WarningsNotAsErrors
Las opciones WarningsAsErrors y WarningsNotAsErrors invalidan la opción TreatWarningsAsErrors de
una lista de advertencias.
Habilite las advertencias 0219 y 0168 como errores:
<WarningsAsErrors>0219,0168</WarningsAsErrors>
<WarningsNotAsErrors>0219,0168</WarningsNotAsErrors>
DisabledWarnings
La opción DisabledWarnings permite impedir que el compilador muestre una o más advertencias. Separe
varios números de advertencia con una coma.
<DisabledWarnings>number1, number2</DisabledWarnings>
number1 , number2 Números de advertencia que quiere que el compilador suprima. Debe especificar la parte
numérica del identificador de advertencia. Por ejemplo, si quiere suprimir CS0028, podría especificar
<DisabledWarnings>28</DisabledWarnings> . El compilador omitirá silenciosamente los números de advertencia
pasados a DisabledWarnings que eran válidos en versiones anteriores, pero que se han quitado. Por ejemplo,
CS0679 era válido en el compilador de Visual Studio .NET 2002, pero se ha eliminado posteriormente.
Las advertencias siguientes no se pueden suprimir mediante la opción DisabledWarnings :
Advertencia del compilador (nivel 1) CS2002
Advertencia del compilador (nivel 1) CS2023
Advertencia del compilador (nivel 1) CS2029
CodeAnalysisRuleSet
Especifica un archivo de conjunto de reglas que configura diagnósticos específicos.
<CodeAnalysisRuleSet>MyConfiguration.ruleset</CodeAnalysisRuleSet>
MyConfiguration.ruleset es la ruta del archivo de conjunto de reglas. Para obtener más información sobre el
uso de conjuntos de reglas, vea el artículo en la documentación de Visual Studio sobre conjuntos de reglas.
ErrorLog
Especifique un archivo para registrar todos los diagnósticos del compilador y el analizador.
<ErrorLog>compiler-diagnostics.sarif</ErrorLog>
La opción ErrorLog hace que el compilador genere un registro de formato de intercambio de resultados de
análisis estático (SARIF). Los registros SARIF suelen ser leídos por herramientas que analizan los resultados de
los diagnósticos del compilador y el analizador.
ReportAnalyzer
Notifica información adicional del analizador, como el tiempo de ejecución.
<ReportAnalyzer>true</ReportAnalyzer>
La opción Repor tAnalyzer hace que el compilador emita información de registro de MSBuild adicional en la
que se detallan las características de rendimiento de los analizadores de la compilación. Los creadores del
analizador la usan normalmente como parte de la validación del analizador.
Opciones del compilador de C# para controlar la
generación de código
16/09/2021 • 5 minutes to read
Las opciones siguientes controlan la generación de código mediante el compilador. La nueva sintaxis de MSBuild
se muestra en negrita . La sintaxis de csc.exe anterior se muestra en code style .
DebugType / -debug : se emite (o no se emite) información de depuración.
Optimize / -optimize : se habilitan las optimizaciones.
Deterministic / -deterministic : se genera una salida equivalente byte a byte del mismo origen de entrada.
ProduceOnlyReferenceAssembly / -refonly : se genera un ensamblado de referencia, en lugar de un
ensamblado completo, como resultado principal.
DebugType
La opción DebugType hace que el compilador genere información de depuración y la incluya en el archivo o
los archivos de salida. La información de depuración se agrega de forma predeterminada para la configuración
de compilación de depuración. Está desactivada de forma predeterminada para la configuración de compilación
de versión.
<DebugType>pdbonly</DebugType>
En todas las versiones del compilador a partir de C# 6.0, no hay ninguna diferencia entre pdbonly y full. Elija
pdbonly. Para cambiar la ubicación del archivo .pdb, vea PdbFile .
Valores válidos son:
VA LO R SIGN IF IC A DO
Optimización
La opción Optimización habilita o deshabilita las optimizaciones realizadas por el compilador para que el
archivo de salida sea menor, más rápido y más eficaz. La opción Optimización está habilitada de forma
predeterminada para una configuración de compilación de versión. Está desactivada de forma predeterminada
para una configuración de compilación de depuración.
<Optimize>true</Optimize>
Determinista
Hace que el compilador genere un ensamblado cuya salida byte a byte es idéntica en todas las compilaciones
para las entradas idénticas.
<Deterministic>true</Deterministic>
De forma predeterminada, la salida del compilador de un conjunto dado de entradas es única, ya que el
compilador agrega una marca de tiempo y un MVID que se genera a partir de números aleatorios. Use la opción
<Deterministic> para generar un ensamblado determinista, cuyo contenido binario es idéntico en todas las
compilaciones, siempre y cuando la entrada siga siendo la misma. En este tipo de compilación, los campos
timestamp y MVID se reemplazarán por los valores derivados de un hash de todas las entradas de la
compilación. El compilador tiene en cuenta las siguientes entradas que afectan al determinismo:
La secuencia de parámetros de la línea de comandos.
El contenido del archivo de respuesta .rsp del compilador.
La versión exacta del compilador que se usa y los ensamblados a los que se hace referencia.
La ruta de acceso del directorio actual.
El contenido binario de todos los archivos pasados explícitamente al compilador, ya sea de manera directa o
indirecta, incluidos los siguientes:
Archivos de código fuente
Ensamblados a los que se hace referencia
Módulos a los que se hace referencia
Recursos
Archivo de clave de nombre seguro
Archivos de respuesta @
Analizadores
Conjuntos de reglas
Otros archivos que pueden usar los analizadores
La referencia cultural actual (para el idioma en el que se producen los diagnósticos y los mensajes de
excepción).
La codificación predeterminada (o página de códigos actual) si no se especifica la codificación.
La existencia (o la inexistencia) de archivos y su contenido en las rutas de búsqueda del compilador
(especificada, por ejemplo, mediante -lib o -recurse ).
La plataforma de Common Language Runtime (CLR) en la que se ejecuta el compilador.
El valor de %LIBPATH% , que pueden afectar a la carga de dependencias del analizador.
Se puede usar la compilación determinista para establecer si un archivo binario se compila a partir de un origen
de confianza. La salida determinista puede ser útil cuando el origen está disponible públicamente. También
puede determinar si los pasos de compilación dependen de los cambios en los binarios que se usan en el
proceso de compilación.
ProduceOnlyReferenceAssembly
La opción ProduceOnlyReferenceAssembly indica que un ensamblado de referencia se debe mostrar en
lugar de un ensamblado de implementación, como el resultado principal. El parámetro
ProduceOnlyReferenceAssembly deshabilita de forma automática la generación de archivos PDB, ya que los
ensamblados de referencia no se pueden ejecutar.
<ProduceOnlyReferenceAssembly>true</ProduceOnlyReferenceAssembly>
Los ensamblados de referencia son un tipo especial de ensamblado. Los ensamblados de referencia solo
contienen la cantidad mínima de metadatos necesarios para representar la superficie de API públicas de la
biblioteca. Incluyen declaraciones para todos los miembros que son significativos al hacer referencia a un
ensamblado en las herramientas de compilación, pero excluyen todas las implementaciones de miembros y las
declaraciones de miembros privados que no tienen ningún impacto observable en su contrato de API. Para
obtener más información, vea Ensamblados de referencia.
Las opciones ProduceOnlyReferenceAssembly y ProduceReferenceAssembly son mutuamente
excluyentes.
Opciones del compilador de C# para opciones de
seguridad
16/09/2021 • 5 minutes to read
Las opciones siguientes controlan las opciones de seguridad del compilador. La nueva sintaxis de MSBuild se
muestra en negrita . La sintaxis de csc.exe anterior se muestra en code style .
PublicSign / -publicsign : se firma el ensamblado públicamente.
DelaySign / -delaysign : se retrasa la firma del ensamblado usando solo la parte pública de la clave de
nombre seguro.
KeyFile / -keyfile : se especifica un archivo de clave con nombre seguro.
KeyContainer / -keycontainer : se especifica un contenedor de claves con nombre seguro.
HighEntropyVA / -highentropyva : se habilita la selección aleatoria del diseño del espacio de direcciones
(ASLR) de alta entropía.
PublicSign
Esta opción hace que el compilador aplique una clave pública pero no firma el ensamblado. La opción
PublicSign también establece un bit en el ensamblado que indica al entorno de ejecución que el archivo está
firmado.
<PublicSign>true</PublicSign>
Para la opción PublicSign es necesario usar la opción KeyFile o KeyContainer . Las opciones keyFile y
KeyContainer especifican la clave pública. Las opciones PublicSign y DelaySign son mutuamente
excluyentes. A veces llamada "firma falsa" o "firma OSS", la firma pública incluye la clave pública en un
ensamblado de salida y establece la marca "signed". La firma pública no firma realmente el ensamblado con una
clave privada. Los desarrolladores usan el signo público para los proyectos de código abierto. Los usuarios
crean ensamblados que son compatibles con los ensamblados "totalmente firmados" publicados cuando no
tienen acceso a la clave privada utilizada para firmar los ensamblados. Como prácticamente ningún consumidor
necesita realmente comprobar si el ensamblado está firmado de forma completa, estos ensamblados creados de
forma pública se pueden utilizar en casi todos los escenarios en los que se utilizaría el ensamblado totalmente
firmado.
DelaySign
Esta opción hace que el compilador reserve espacio en el archivo de salida de manera que se pueda agregar una
firma digital más adelante.
<DelaySign>true</DelaySign>
Use DelaySign- si quiere un ensamblado completamente firmado. Use DelaySign si solo quiere incluir la clave
pública en el ensamblado. La opción DelaySign no tiene ningún efecto a menos que se use con KeyFile o
KeyContainer . Las opciones KeyContainer y PublicSign son mutuamente excluyentes. Cuando se solicita un
ensamblado totalmente firmado, el compilador genera un valor hash para el archivo que contiene el manifiesto
(metadatos del ensamblado) y firma dicho valor mediante la clave privada. Esta operación crea una firma digital
que se almacena en el archivo que contiene el manifiesto. Cuando se retrasa la firma de un ensamblado, el
compilador no procesa ni almacena la firma. En su lugar, reserva espacio en el archivo para que la firma se
pueda agregar después.
El uso de DelaySign permite a un evaluador colocar el ensamblado en la caché global. Después de realizar las
pruebas, coloque la clave privada en el ensamblado mediante la utilidad Assembly Linker para firmar el
ensamblado por completo. Para obtener más información, vea Crear y usar ensamblados con nombre seguro y
Retraso de la firma de un ensamblado.
KeyFile
Especifica el nombre de archivo que contiene la clave criptográfica.
<KeyFile>filename</KeyFile>
file es el nombre del archivo que contiene la clave de nombre seguro. Cuando se usa esta opción, el
compilador inserta la clave pública del archivo especificado en el manifiesto del ensamblado y, después, firma el
último ensamblado con la clave privada. Para generar un archivo de claves, escriba sn -k file en la línea de
comandos. Si se compila con -target:module , el nombre del archivo de claves se mantiene en el módulo y se
incorpora en el ensamblado que se crea al compilarlo con AddModules . También puede pasar la información
de cifrado al compilador con Keycontainer . Use DelaySign si quiere firmar el ensamblado de forma parcial. Si
se especifica tanto KeyFile como KeyContainer en la misma compilación, el compilador probará primero el
contenedor de claves. Si lo consigue, el ensamblado se firma con la información del contenedor de claves. Si el
compilador no encuentra el contenedor de claves, probará el archivo especificado con KeyFile . Si la operación
es correcta, el ensamblado se firma con la información del archivo de clave y la información de la clave se
instalará en el contenedor de claves. En la siguiente compilación, el contenedor de claves será válido. Es posible
que un archivo de clave solo contenga la clave pública. Para obtener más información, vea Crear y usar
ensamblados con nombre seguro y Retraso de la firma de un ensamblado.
KeyContainer
Especifica el nombre del contenedor de claves criptográficas.
<KeyContainer>container</KeyContainer>
container es el nombre del contenedor de claves de nombre seguro. Cuando se usa la opción KeyContainer ,
el compilador crea un componente que se puede compartir. El compilador inserta una clave pública del
contenedor especificado en el manifiesto del ensamblado y firma el último ensamblado con la clave privada.
Para generar un archivo de claves, escriba sn -k file en la línea de comandos. sn -i instala el par de claves
en un contenedor. Esta opción no se admite cuando el compilador se ejecuta en CoreCLR. Para firmar un
ensamblado al compilar en CoreCLR, use la opción KeyFile . Si realiza la compilación con TargetType , el
nombre del archivo de claves se mantiene en el módulo y se incorpora al ensamblado al compilar este módulo
en un ensamblado con AddModules . También se puede especificar esta opción como un atributo personalizado
(System.Reflection.AssemblyKeyNameAttribute) en el código fuente de cualquier módulo del Lenguaje
Intermedio de Microsoft (MSIL). También se puede pasar la información de cifrado al compilador con KeyFile .
Use DelaySign para agregar la clave pública al manifiesto del ensamblado pero firmarlo cuando se haya
probado. Para obtener más información, vea Crear y usar ensamblados con nombre seguro y Retraso de la
firma de un ensamblado.
HighEntropyVA
La opción del compilador HighEntropyVA indica al kernel de Windows si un archivo ejecutable determinado
admite la selección aleatoria del diseño del espacio de direcciones (ASLR) de alta entropía.
<HighEntropyVA>true</HighEntropyVA>
Esta opción especifica que un archivo ejecutable de 64 bits o un archivo ejecutable marcado con la opción del
compilador PlatformTarget admite un espacio de direcciones virtuales de alta entropía. La opción está
deshabilitada de forma predeterminada. Use HighEntropyVA para habilitarla.
La opción HighEntropyVA habilita las versiones compatibles del kernel de Windows para usar niveles
superiores de entropía al aleatorizar el diseño del espacio de direcciones de un proceso como parte de ASLR. El
uso de niveles superiores de entropía significa que un mayor número de direcciones se puede asignar a las
regiones de memoria como pilas y montones. Como resultado, la ubicación de un área de memoria concreta es
más difícil de adivinar. La opción del compilador HighEntropyVA necesita que el archivo ejecutable de destino
y todos los módulos de los que depende puedan controlar los valores de puntero superiores a 4 gigabytes (GB),
cuando se ejecutan como un proceso de 64 bits.
Opciones del compilador de C# que especifican
recursos
16/09/2021 • 5 minutes to read
Las opciones siguientes controlan cómo el compilador de C# crea o importa recursos de Win32. La nueva
sintaxis de MSBuild se muestra en negrita . La sintaxis de csc.exe anterior se muestra en code style .
Win32Resource / -win32res : se especifica un archivo de recursos (.res) de Win32.
Win32Icon / -win32icon : se hace referencia a los metadatos de los archivos de ensamblado especificados.
Win32Manifest / -win32manifest :se especifica un archivo de manifiesto (.xml) de Win32.
NoWin32Manifest / -nowin32manifest : no se incluye el manifiesto de Win32 predeterminado.
Resources / -resource : se inserta el recurso especificado (forma abreviada: /res).
LinkResources / -linkresources :se vincula el recurso especificado a este ensamblado.
Win32Resource
La opción Win32Resource inserta un recurso de Win32 en el archivo de salida.
<Win32Resource>filename</Win32Resource>
filename es el archivo de recursos que se quiere agregar al archivo de salida. Un recurso de Win32 puede
contener información de versión o de mapa de bits (icono) que ayudaría a identificar la aplicación en el
Explorador de archivos. Si no especifica esta opción, el compilador generará información de versión en función
de la versión del ensamblado.
Win32Icon
La opción Win32Icon inserta un archivo .ico en el archivo de salida, lo que proporciona al archivo de salida la
apariencia esperada en el Explorador de archivos.
<Win32Icon>filename</Win32Icon>
filename es el archivo .ico que quiere agregar al archivo de salida. Se puede crear un archivo .ico con el
Compilador de recursos. El Compilador de recursos se invoca al compilar un programa de Visual C++ y se crea
un archivo .ico a partir del archivo .rc.
Win32Manifest
Use la opción Win32Manifest para especificar un archivo de manifiesto de aplicación Win32 definido por el
usuario que se va a insertar en un archivo portable ejecutable (PE) del proyecto.
<Win32Manifest>filename</Win32Manifest>
NOTE
Esta opción y Win32Resources son mutuamente excluyentes. Si intenta usar ambas en la misma línea de comandos,
obtendrá un error de compilación.
Una aplicación sin manifiesto de aplicación que especifique un nivel de ejecución solicitado estará sujeta a
virtualización de archivos y Registro conforme a la característica Control de cuentas de usuario de Windows.
Para más información, vea User Account Control (Control de cuentas de usuario).
La aplicación estará sujeta a virtualización si se cumple cualquiera de estas condiciones:
Se usa la opción NoWin32Manifest y no se proporciona ningún manifiesto en un paso de compilación
posterior o como parte de un archivo de recursos de Windows ( .res) mediante la opción Win32Resource .
Se proporciona un manifiesto personalizado que no especifica un nivel de ejecución solicitado.
Visual Studio crea un archivo .manifest predeterminado y lo almacena en los directorios de depuración y versión
junto con el archivo ejecutable. Puede agregar un manifiesto personalizado si crea uno en cualquier editor de
texto y luego lo agrega al proyecto. O bien, puede hacer clic con el botón derecho en el icono Proyecto del
Explorador de soluciones , seleccionar Agregar nuevo elemento y luego Archivo de manifiesto de
aplicación . Después de haber agregado el archivo de manifiesto nuevo o existente, aparecerá en la lista
desplegable Manifiesto . Para más información, vea Página de aplicación, Diseñador de proyectos (C#).
Puede proporcionar el manifiesto de aplicación como un paso personalizado posterior a la compilación o como
parte de un archivo de recursos Win32 con la opción NoWin32Manifest . Use esa misma opción si quiere que
la aplicación esté sujeta a virtualización de archivos y Registro en Windows Vista.
NoWin32Manifest
Use la opción NoWin32Manifest para indicar al compilador que no inserte ningún manifiesto de aplicación en
el archivo ejecutable.
<NoWin32Manifest />
Cuando se use esta opción, la aplicación estará sujeta a la virtualización en Windows Vista a menos que
proporcione un manifiesto de aplicación en un archivo de recursos Win32 o durante un paso de compilación
posterior.
En Visual Studio, defina esta opción en la página de propiedades de la aplicación seleccionando la opción
Crear aplicación sin manifiesto en la lista desplegable Manifiesto . Para más información, vea Página de
aplicación, Diseñador de proyectos (C#).
Recursos
Inserta el recurso especificado en el archivo de salida.
<Resources Include=filename>
<LogicalName>identifier</LogicalName>
<Access>accessibility-modifier</Access>
</Resources>
filename es el archivo de recursos de .NET que quiere insertar en el archivo de salida. identifier (opcional) es
el nombre lógico del recurso, que se usa para cargarlo. El valor predeterminado es el nombre del archivo.
accessibility-modifier (opcional) es la accesibilidad del recurso: pública o privada. El valor predeterminado es
public. De manera predeterminada, los recursos son públicos en el ensamblado cuando se crean mediante el
compilador de C#. Para que sean privados, especifique el modificador de accesibilidad private . No se permite
ninguna otra accesibilidad distinta de public o private . Si filename es un archivo de recursos de .NET creado,
por ejemplo, con Resgen.exe o en el entorno de desarrollo, se puede acceder a él con miembros del espacio de
nombres System.Resources. Para obtener más información, vea System.Resources.ResourceManager. Para todos
los demás recursos, use los métodos GetManifestResource de la clase Assembly para tener acceso al recurso en
tiempo de ejecución. El orden de los recursos en el archivo de salida se determina a partir del orden
especificado en el archivo del proyecto.
LinkResources
Crea un vínculo a un recurso de .NET en el archivo de salida. El archivo de recursos no se agrega al archivo de
salida. LinkResources difiere de la opción Resource , que sí inserta un archivo de recursos en el archivo de
salida.
<LinkResources Include=filename>
<LogicalName>identifier</LogicalName>
<Access>accessibility-modifier</Access>
</LinkResources>
filename es el archivo de recursos de .NET con el que quiere crear un vínculo desde el ensamblado. identifier
(opcional) es el nombre lógico del recurso, que se usa para cargarlo. El valor predeterminado es el nombre del
archivo. accessibility-modifier (opcional) es la accesibilidad del recurso: pública o privada. El valor
predeterminado es public. De manera predeterminada, los recursos vinculados son públicos en el ensamblado
cuando se crean con el compilador de C#. Para que sean privados, especifique el modificador de accesibilidad
private . No se permite ningún otro modificador distinto de public o private . Si filename es un archivo de
recursos de .NET creado, por ejemplo, con Resgen.exe o en el entorno de desarrollo, se puede acceder a él con
miembros del espacio de nombres System.Resources. Para obtener más información, vea
System.Resources.ResourceManager. Para todos los demás recursos, use los métodos GetManifestResource de la
clase Assembly para tener acceso al recurso en tiempo de ejecución. El archivo especificado en filename puede
tener cualquier formato. Por ejemplo, se puede hacer que una DLL nativa forme parte de un ensamblado para
que se pueda instalar en la caché global de ensamblados y sea accesible desde código administrado del
ensamblado. También es posible realizar lo mismo en Assembly Linker. Para obtener más información, vea Al.exe
(Assembly Linker) y Trabajar con ensamblados y la memoria caché global de ensamblados.
Otras opciones del compilador de C#
16/09/2021 • 2 minutes to read
Las opciones siguientes controlan distintos comportamientos del compilador. La nueva sintaxis de MSBuild se
muestra en negrita . La sintaxis de csc.exe anterior se muestra en code style .
ResponseFiles / -@ : se lee el archivo de respuesta para ver más opciones.
NoLogo / -nologo : se suprime el mensaje de copyright del compilador.
NoConfig / -noconfig : no se incluye el archivo CSC.RSP de forma automática.
ResponseFiles
La opción ResponseFiles le permite especificar un archivo que contiene opciones del compilador y archivos de
código fuente para compilar.
<ResponseFiles>response_file</ResponseFiles>
response_file especifica el archivo en el que se enumeran las opciones del compilador o los archivos de código
fuente que se van a compilar. El compilador procesará las opciones del compilador y los archivos de código
fuente como si se hubieran especificado en la línea de comandos. Para especificar más de un archivo de
respuesta en una compilación, especifique varias opciones de archivo de respuesta. En un archivo de respuesta,
puede haber varias opciones del compilador y archivos de código fuente en una sola línea. Una sola
especificación de opción del compilador debe aparecer en una línea (no puede abarcar varias). Los archivos de
respuesta pueden tener comentarios que comiencen con el símbolo #. Especificar opciones del compilador
desde un archivo de respuesta es igual que enviar esos comandos en la línea de comandos. El compilador
procesa las opciones de comando a medida que se leen. Los argumentos de línea de comandos pueden
reemplazar las opciones enumeradas anteriormente en archivos de respuesta. Por el contrario, las opciones en
un archivo de respuesta reemplazarán las opciones enumeradas anteriormente en la línea de comandos o en
otros archivos de respuesta. C# proporciona el archivo csc.rsp, que se encuentra en el mismo directorio que el
archivo csc.exe. Para obtener más información sobre el formato de archivo de respuesta, vea NoConfig . No se
puede establecer esta opción del compilador en el entorno de desarrollo de Visual Studio, ni se puede cambiar
mediante programación. Estas son algunas líneas de un archivo de respuesta de ejemplo:
NoLogo
La opción NoLogo suprime la presentación del banner de inicio de sesión cuando se inicia el compilador y
muestra mensajes informativos durante la compilación.
<NoLogo>true</NoLogo>
NoConfig
La opción NoConfig indica al compilador que no realice la compilación con el archivo csc.rsp.
<NoConfig>true</NoConfig>
El archivo csc.rsp hace referencia a todos los ensamblados incluidos en .NET Framework. Las referencias reales
que incluye el entorno de desarrollo Visual Studio de .NET dependen del tipo de proyecto. Es posible modificar
el archivo csc.rsp y especificar opciones del compilador adicionales que se deban incluir en todas las
compilaciones. Si no quiere que el compilador busque y use la configuración del archivo csc.rsp, especifique
NoConfig . Esta opción del compilador no está disponible en Visual Studio y no se puede cambiar mediante
programación.
Opciones avanzadas del compilador de C#
16/09/2021 • 11 minutes to read
Las opciones siguientes admiten escenarios avanzados. La nueva sintaxis de MSBuild se muestra en negrita . La
sintaxis de csc.exe anterior se muestra en code style .
MainEntr yPoint , Star tupObject / -main : se especifica el tipo que contiene el punto de entrada.
PdbFile / -pdb : se especifica el nombre del archivo de información de depuración.
PathMap / -pathmap : se especifica una asignación para los nombres de rutas de acceso de origen
generados por el compilador.
ApplicationConfiguration / -appconfig : se especifica un archivo de configuración de la aplicación que
contenga la configuración de enlace de ensamblados.
AdditionalLibPaths / -lib : se especifican los directorios adicionales en los que buscar referencias.
GenerateFullPaths / -fullpath : el compilador genera rutas de acceso completas.
PreferredUILang / -preferreduilang : se especifica el nombre del lenguaje de salida preferido.
BaseAddress / -baseaddress : se especifica la dirección base de la biblioteca que se va a compilar.
ChecksumAlgorithm / -checksumalgorithm : se especifica el algoritmo para calcular la suma de
comprobación del archivo de origen almacenada en el archivo PDB.
CodePage / -codepage : se especifica la página de códigos que se va a usar al abrir los archivos de código
fuente.
Utf8Output / -utf8output : se muestran los mensajes del compilador en codificación UTF-8.
FileAlignment / -filealign : se especifica la alineación utilizada para las secciones de archivos de salida.
ErrorEndLocation / -errorendlocation : se muestra la línea y la columna de salida de la ubicación final de
cada error.
NoStandardLib / -nostdlib : no se hace referencia a la biblioteca estándar mscorlib.dll.
SubsystemVersion / -subsystemversion : se especifica la versión del subsistema de este ensamblado.
ModuleAssemblyName / -moduleassemblyname : nombre del ensamblado del que este módulo formará
parte.
MainEntryPoint o StartupObject
Esta opción especifica la clase que contiene el punto de entrada al programa si hay más de una clase que
contenga un método Main .
<StartupObject>MyNamespace.Program</StartupObject>
or
<MainEntryPoint>MyNamespace.Program</MainEntryPoint>
Donde Program es el tipo que contiene el método Main . El nombre de clase proporcionado debe ser completo:
debe incluir el espacio de nombres completo que contiene la clase, seguido del nombre de clase. Por ejemplo,
cuando el método Main se encuentra dentro de la clase Program en el espacio de nombres MyApplication.Core
, la opción del compilador tiene que ser -main:MyApplication.Core.Program . Si la compilación incluye más de un
tipo con un método Main , puede especificar qué tipo contiene el método Main .
NOTE
Esta opción no se puede usar para un proyecto que incluya instrucciones de nivel superior, incluso si contiene uno o más
métodos Main .
PdbFile
La opción del compilador PdbFile especifica el nombre y la ubicación del archivo de símbolos de depuración. El
valor filename especifica el nombre y la ubicación del archivo de símbolos de depuración.
<PdbFile>filename</PdbFile>
Al especificar DebugType , el compilador creará un archivo .pdb en el mismo directorio donde creará el archivo
de salida (.exe o .dll). El archivo .pdb tiene el mismo nombre de archivo base que el del archivo de salida.
PdbFile le permite especificar un nombre de archivo y una ubicación distintos a los predeterminados para el
archivo .pdb. No se puede establecer esta opción del compilador en el entorno de desarrollo de Visual Studio, ni
se puede cambiar mediante programación.
PathMap
La opción del compilador PathMap especifica cómo asignar rutas de acceso físicas a los nombres de rutas de
acceso de origen generados por el compilador. Esta opción asigna cada ruta de acceso física en la máquina
donde el compilador se ejecuta a una ruta de acceso correspondiente que debe escribirse en los archivos de
salida. En el ejemplo siguiente, path1 es la ruta completa a los archivos de código fuente en el entorno actual y
sourcePath1 es la ruta de origen sustituida por path1 en cualquier archivo de salida. Para especificar varias
rutas de acceso de origen asignadas, sepárelas mediante punto y coma.
<PathMap>path1=sourcePath1;path2=sourcePath2</PathMap>
El compilador escribe la ruta de acceso de origen en la salida por las razones siguientes:
1. La ruta de acceso de origen se sustituye por un argumento cuando se aplica CallerFilePathAttribute a un
parámetro opcional.
2. La ruta de acceso de origen se inserta en un archivo PDB.
3. La ruta de acceso del archivo PDB se inserta en un archivo PE (ejecutable portable).
ApplicationConfiguration
La opción del compilador ApplicationConfiguration permite a una aplicación de C# especificar la ubicación
del archivo de configuración de la aplicación de un ensamblado (app.config) al Common Language Runtime
(CLR) en tiempo de enlace del ensamblado.
<ApplicationConfiguration>file</ApplicationConfiguration>
file es el archivo de configuración de la aplicación que contiene los valores de enlace del ensamblado. Un uso
de ApplicationConfiguration es para escenarios avanzados en los que un ensamblado tiene que hacer
referencia a la versión de .NET Framework y a la de .NET Framework para Silverlight de un ensamblado de
referencia determinado al mismo tiempo. Por ejemplo, un diseñador de XAML escrito en Windows Presentation
Foundation (WPF) podría tener que hacer referencia al escritorio de WPF, para la interfaz de usuario del
diseñador, y al subconjunto de WPF que se incluye con Silverlight. El mismo ensamblado del diseñador tiene
que tener acceso a ambos ensamblados. De forma predeterminada, las referencias independientes producen un
error del compilador, ya que el enlace del ensamblado considera los dos ensamblados equivalentes. La opción
del compilador ApplicationConfiguration permite especificar la ubicación de un archivo app.config que
deshabilita el comportamiento predeterminado mediante una etiqueta <supportPortability> , como se muestra
en el ejemplo siguiente.
El compilador pasa la ubicación del archivo a la lógica de enlace del ensamblado de CLR.
NOTE
Para usar el archivo app.config que ya está establecido en el proyecto, agregue la etiqueta de propiedades
<UseAppConfigForCompiler> al archivo .csproj y establezca su valor en true . Para especificar otro archivo app.config,
agregue la etiqueta <AppConfigForCompiler> de propiedades y establezca su valor en la ubicación del archivo.
En el siguiente ejemplo se muestra un archivo app.config que permite a una aplicación tener referencias a la
implementación de .NET Framework y de .NET Framework para Silverlight de cualquier ensamblado de .NET
Framework que exista en ambas implementaciones. La opción del compilador ApplicationConfiguration
especifica la ubicación de este archivo app.config.
<configuration>
<runtime>
<assemblyBinding>
<supportPortability PKT="7cec85d7bea7798e" enable="false"/>
<supportPortability PKT="31bf3856ad364e35" enable="false"/>
</assemblyBinding>
</runtime>
</configuration>
AdditionalLibPaths
La opción AdditionalLibPaths especifica la ubicación de los ensamblados a los que se hace referencia con la
opción References .
<AdditionalLibPaths>dir1[,dir2]</AdditionalLibPaths>
dir1 es un directorio donde el compilador busca si un ensamblado al que se hace referencia no se encuentra
en el directorio de trabajo actual (el directorio desde el que se invoca al compilador) o en el directorio del
sistema de Common Language Runtime. dir2 es uno o varios directorios adicionales en los que buscar
referencias a ensamblados. Separe los nombres de directorio con una coma y sin espacio en blanco entre ellos.
El compilador busca referencias a ensamblados que no presentan la ruta completa en el siguiente orden:
1. Directorio de trabajo actual.
2. El directorio del sistema de Common Language Runtime.
3. Directorios especificados por AdditionalLibPaths .
4. Directorios especificados por la variable de entorno LIB.
Use Reference para especificar una referencia a un ensamblado. AdditionalLibPaths es aditivo. Si se
especifica más de una vez, se anexa a los valores existentes. Como en el manifiesto del ensamblado no se
especifica la ruta al ensamblado dependiente, la aplicación buscará y usará el ensamblado en la caché global de
ensamblados. El compilador que hace referencia al ensamblado no implica que el Common Language Runtime
pueda encontrar y cargar el ensamblado en tiempo de ejecución. Vea Cómo el motor en tiempo de ejecución
ubica ensamblados para obtener los detalles sobre cómo busca el motor en tiempo de ejecución los
ensamblados a los que se hace referencia.
GenerateFullPaths
La opción GenerateFullPaths hace que el compilador especifique la ruta completa al archivo cuando se
muestran los errores de compilación y las advertencias.
<GenerateFullPaths>true</GenerateFullPaths>
De manera predeterminada, los errores y advertencias que se producen de la compilación especifican el nombre
del archivo en el que se ha detectado el error. La opción GenerateFullPaths hace que el compilador especifique
la ruta completa al archivo. Esta opción del compilador no está disponible en Visual Studio y no se puede
cambiar mediante programación.
PreferredUILang
Mediante la opción del compilador PreferredUILang puede especificar el idioma en el que el compilador de C#
muestra el resultado, como los mensajes de error.
<PreferredUILang>language</PreferredUILang>
language es el nombre del idioma que se va a usar para la salida del compilador. Puede usar la opción del
compilador PreferredUILang para especificar el idioma que quiere que use el compilador de C# para los
mensajes de error y otra salida de la línea de comandos. Si el paquete de idioma para el idioma no está
instalado, en su lugar se usa la configuración de idioma del sistema operativo.
BaseAddress
La opción BaseAddress permite especificar la dirección base preferida en la que cargar un archivo DLL. Para
obtener más información sobre cuándo y por qué usar esta opción, vea el blog de Larry Osterman.
<BaseAddress>address</BaseAddress>
address es la dirección base del archivo DLL. Esta dirección puede especificarse como un número octal,
hexadecimal o decimal. La dirección base predeterminada para un archivo DLL se establece mediante Common
Language Runtime de .NET. La palabra de orden inferior en esta dirección se redondeará. Por ejemplo, si
especifica 0x11110001 , se redondeará a 0x11110000 . Para completar el proceso de firma para un archivo DLL,
use SN.EXE con la opción -R.
ChecksumAlgorithm
Esta opción controla el algoritmo de suma de comprobación que se usa para codificar los archivos de código
fuente en el archivo PDB.
<ChecksumAlgorithm>algorithm</ChecksumAlgorithm>
CodePage
Esta opción especifica qué página de códigos se va a usar durante la compilación si la página necesaria no es la
página de códigos predeterminada actual del sistema.
<CodePage>id</CodePage>
id es el id. de la página de códigos que se va a usar para todos los archivos de código fuente de la compilación.
El compilador primero intenta interpretar todos los archivos de código fuente como UTF-8. Si los archivos de
código fuente se encuentran en una codificación distinta de UTF-8 y usan caracteres que no sean ASCII de 7 bits,
utilice la opción CodePage para especificar qué página de códigos se debe usar. CodePage se aplica a todos
los archivos de código fuente de la compilación. Vea GetCPInfo para obtener información sobre cómo buscar las
páginas de códigos que se admiten en su sistema.
Utf8Output
La opción Utf8Output muestra los resultados del compilador en codificación UTF-8.
<Utf8Output>true</Utf8Output>
FileAlignment
La opción FileAlignment permite especificar el tamaño de las secciones en el archivo de salida. Los valores
válidos son 512, 1024, 2048, 4096 y 8192. Estos valores están en bytes.
<FileAlignment>number</FileAlignment>
ErrorEndLocation
Indica al compilador que muestre la línea y la columna de salida de la ubicación final de cada error.
<ErrorEndLocation>filename</ErrorEndLocation>
De forma predeterminada, el compilador escribe la ubicación inicial en el origen para todos los errores y
advertencias. Cuando esta opción se establece en true, el compilador escribe la ubicación inicial y final para
todos los errores y advertencias.
NoStandardLib
NoStandardLib impide la importación de mscorlib.dll, que define el espacio de nombres System completo.
<NoStandardLib>true</NoStandardLib>
Use esta opción si desea definir o crear sus propios objetos y espacio de nombres System. Si no se especifica
NoStandardLib , mscorlib.dll se importa en el programa (equivale a especificar
<NoStandardLib>false</NoStandardLib> ).
SubsystemVersion
Especifica la versión mínima del subsistema en el que se ejecuta el archivo ejecutable. Normalmente, esta
opción garantiza que el archivo ejecutable pueda usar características de seguridad que no están disponibles en
versiones anteriores de Windows.
NOTE
Para especificar el subsistema en sí mismo, use la opción del compilador TargetType .
<SubsystemVersion>major.minor</SubsystemVersion>
major.minor especifica la versión mínima necesaria del subsistema, expresada en una notación de puntos para
las versiones principales y secundarias. Por ejemplo, puede especificar que una aplicación no se pueda ejecutar
en un sistema operativo anterior a Windows 7. Establezca el valor de esta opción en 6.01, como se describe en la
tabla que se muestra más adelante en este artículo. Especifique los valores de major y minor como números
enteros. Los ceros a la izquierda en la versión minor no cambian la versión, pero los ceros a la derecha sí. Por
ejemplo, 6.1 y 6.01 hacen referencia a la misma versión, pero 6.10 hace referencia a una versión diferente. Se
recomienda expresar la versión secundaria como dos dígitos para evitar confusiones.
En la tabla siguiente se enumeran las versiones de subsistema habituales de Windows.
Windows 7 6.01
Windows 8 6.02
El valor predeterminado de la opción del compilador SubsystemVersion depende de las condiciones de la lista
siguiente:
El valor predeterminado es 6.02 si se establece cualquier opción del compilador en la siguiente lista:
/target:appcontainerexe
/target:winmdobj
-platform:arm
El valor predeterminado es 6,00 si usa MSBuild, tiene como destino .NET Framework 4.5 y no ha configurado
ninguna de las opciones del compilador que se han especificado anteriormente en esta lista.
El valor predeterminado es 4.00 si no se cumple ninguna de las condiciones anteriores.
ModuleAssemblyName
Especifica el nombre de un ensamblado con tipos no públicos a los que puede acceder un archivo .netmodule.
<ModuleAssemblyName>assembly_name</ModuleAssemblyName>
Los archivos de origen de C# pueden tener comentarios estructurados que generan documentación de API para
los tipos definidos en esos archivos. El compilador de C# genera un archivo XML que contiene datos
estructurados que representan los comentarios y las firmas de API. Otras herramientas pueden procesar esa
salida XML para crear documentación legible en forma de páginas web o archivos PDF, por ejemplo.
Este proceso proporciona muchas ventajas para agregar documentación de API en el código:
El compilador de C# combina la estructura del código de C# con el texto de los comentarios en un único
documento XML.
El compilador de C# comprueba que los comentarios coinciden con las firmas de API para las etiquetas
pertinentes.
Las herramientas que procesan los archivos de documentación XML pueden definir elementos y atributos
XML específicos de esas herramientas.
Herramientas como Visual Studio proporcionan IntelliSense para muchos elementos XML comunes que se usan
en los comentarios de documentación.
En este artículo se tratan los siguientes temas:
Comentarios de documentación y generación de archivos XML
Etiquetas validadas por el compilador de C# y Visual Studio
Formato del archivo XML generado
/// <summary>
/// This class performs an important function.
/// </summary>
public class MyClass {}
Establezca la opción DocumentationFile y el compilador encontrará todos los campos de comentario con
etiquetas XML en el código fuente y creará un archivo de documentación XML a partir de esos comentarios.
Cuando esta opción está habilitada, el compilador genera la advertencia CS1591 para cualquier miembro visible
públicamente declarado en el proyecto sin comentarios de documentación XML.
Delimitadores de varias líneas /** */ : los delimitadores /** */ tienen las siguientes reglas de formato:
En la línea que contiene el delimitador /** , si el resto de la línea es un espacio en blanco, la línea
no se procesa en busca de comentarios. Si el primer carácter después del delimitador /** es un
espacio en blanco, dicho carácter de espacio en blanco se omite y se procesa el resto de la línea. En
caso contrario, todo el texto de la línea situado después del delimitador /** se procesa como
parte del comentario.
En la línea que contiene el delimitador */ , si solo hay un espacio en blanco hasta el delimitador
*/ , esa línea se omite. En caso contrario, el texto de la línea situado antes del delimitador */ se
procesa como parte del comentario.
Para las líneas que aparecen después de la que comienza con el delimitador /** , el compilador
busca un patrón común al principio de cada línea. El patrón puede consistir en un espacio en
blanco opcional y un asterisco ( * ), seguido de otro espacio en blanco opcional. Si el compilador
encuentra un patrón común al principio de cada línea que no comienza con el delimitador /** ni
termina con el delimitador */ , omite ese patrón para cada línea.
La única parte del comentario siguiente que se procesará es la línea que comienza con <summary> .
Los tres formatos de etiquetas producen los mismos comentarios.
/** <summary>text</summary> */
/**
<summary>text</summary>
*/
/**
* <summary>text</summary>
*/
El compilador identifica un patrón común de " * " al principio de la segunda y la tercera línea. El
patrón no se incluye en la salida.
/**
* <summary>
* text </summary>*/
/**
* <summary>
text </summary>
*/
El compilador no encuentra ningún patrón en el comentario siguiente por dos razones. En primer
lugar, el número de espacios antes del asterisco no es coherente. En segundo lugar, la quinta línea
comienza con una pestaña, que no coincide con los espacios. Todo el texto de las líneas dos a cinco
se procesa como parte del comentario.
/**
* <summary>
* text
* text2
* </summary>
*/
Para hacer referencia a elementos XML (por ejemplo, la función procesa los elementos XML concretos que desea
describir en un comentario de documentación XML), puede usar el mecanismo de entrecomillado estándar (
< y > ). Para hacer referencia a identificadores genéricos en elementos de referencia de código ( cref ),
puede usar los caracteres de escape (por ejemplo, cref="List<T>" ) o llaves ( cref="List{T}" ). Como caso
especial, el compilador analiza las llaves como corchetes angulares para que la creación del comentario de
documentación resulte menos complicada al hacer referencia a identificadores genéricos.
NOTE
Los comentarios de documentación XML no son metadatos; no se incluyen en el ensamblado compilado y, por tanto, no
se puede obtener acceso a ellos mediante reflexión.
C A RÁ C T ER T IP O DE M IEM B RO N OTA S
F campo
E event
La segunda parte de la cadena es el nombre completo del elemento, que empieza por la raíz del espacio
de nombres. El nombre del elemento, sus tipos envolventes y el espacio de nombres están separados por
puntos. Si el nombre del elemento ya contiene puntos, estos se reemplazan por el signo hash ("#"). Se
supone que ningún elemento tiene un signo hash directamente en su nombre. Por ejemplo, el nombre
completo del constructor de String es "System.String.#ctor".
Para las propiedades y los métodos, se indica la lista de parámetros entre paréntesis. Si no hay ningún
parámetro, tampoco habrá paréntesis. Los parámetros se separan mediante comas. La codificación de
cada parámetro sigue directamente cómo se codifica en una firma de .NET (consulte
Microsoft.VisualStudio.CorDebugInterop.CorElementType para obtener las definiciones de los elementos
de todos los límites de la lista siguiente):
Tipos base. Los tipos normales ( ELEMENT_TYPE_CLASS o ELEMENT_TYPE_VALUETYPE ) se representan como
el nombre completo del tipo.
Los tipos intrínsecos (por ejemplo, ELEMENT_TYPE_I4 , ELEMENT_TYPE_OBJECT , ELEMENT_TYPE_STRING ,
ELEMENT_TYPE_TYPEDBYREF y ELEMENT_TYPE_VOID ) se representan como el nombre completo del tipo
completo correspondiente. Por ejemplo, System.Int32 o System.TypedReference .
ELEMENT_TYPE_PTR se representa como "*" después del tipo modificado.
ELEMENT_TYPE_BYREF se representa como "@" después del tipo modificado.
ELEMENT_TYPE_CMOD_OPT se representa como "!" y el nombre completo de la clase modificadora,
después del tipo modificado.
ELEMENT_TYPE_SZARRAY se representa como "[]" después del tipo de elemento de la matriz.
ELEMENT_TYPE_ARRAY se representa como [límite inferior : size ,límite inferior : size ], donde el número
de comas es la clasificación - 1, y los límites inferiores y el tamaño de cada dimensión, si se conocen,
se representan en decimales. Si no se especifican un límite inferior ni un tamaño, se omiten. Si se
omiten el límite inferior y el tamaño de una dimensión determinada, también se omite ":". Por
ejemplo, una matriz de dos dimensiones con 1 como los límites inferiores y tamaños no especificados
es [1:,1:].
Solo para los operadores de conversión ( op_Implicit y op_Explicit ), el valor devuelto del método se
codifica como ~ seguido por el tipo de valor devuelto. Por ejemplo:
<member name="M:System.Decimal.op_Explicit(System.Decimal arg)~System.Int32"> es la etiqueta del
operador de conversión public static explicit operator int (decimal value); declarado en la clase
System.Decimal .
Para tipos genéricos, el nombre del tipo está seguido de una tilde aguda y después de un número que
indica el número de parámetros de tipo genérico. Por ejemplo: <member name="T:SampleClass``2"> es la
etiqueta de un tipo que se define como public class SampleClass<T, U> . Para los métodos que toman
tipos genéricos como parámetros, los parámetros de tipo genérico se especifican como números
precedidos por tildes graves (por ejemplo, `0,`1). Cada número representa una notación de matriz de base
cero para los parámetros genéricos del tipo.
ELEMENT_TYPE_PINNED se representa como "^" después del tipo modificado. El compilador de C# nunca
genera esta codificación.
ELEMENT_TYPE_CMOD_REQ se representa como "|" y el nombre completo de la clase modificadora,
después del tipo modificado. El compilador de C# nunca genera esta codificación.
ELEMENT_TYPE_GENERICARRAY se representa como "[?]" después del tipo de elemento de la matriz. El
compilador de C# nunca genera esta codificación.
ELEMENT_TYPE_FNPTR se representa como "=FUNC: type (signature)", donde type es el tipo de valor
devuelto y signature se corresponde con los argumentos del método. Si no hay ningún argumento, se
omiten los paréntesis. El compilador de C# nunca genera esta codificación.
Los siguientes componentes de la firma no se representan porque nunca se usan para diferenciar
métodos sobrecargados:
Convención de llamada
Tipo de valor devuelto
ELEMENT_TYPE_SENTINEL
En los ejemplos siguientes, se muestra cómo se generan las cadenas de identificador para una clase y sus
miembros:
namespace MyNamespace
{
/// <summary>
/// Enter description here for class X.
/// ID string generated is "T:MyNamespace.X".
/// </summary>
public unsafe class MyClass
{
/// <summary>
/// Enter description here for the first constructor.
/// ID string generated is "M:MyNamespace.MyClass.#ctor".
/// </summary>
public MyClass() { }
/// <summary>
/// Enter description here for the second constructor.
/// ID string generated is "M:MyNamespace.MyClass.#ctor(System.Int32)".
/// </summary>
/// <param name="i">Describe parameter.</param>
public MyClass(int i) { }
/// <summary>
/// Enter description here for field message.
/// ID string generated is "F:MyNamespace.MyClass.message".
/// </summary>
public string message;
/// <summary>
/// Enter description for constant PI.
/// ID string generated is "F:MyNamespace.MyClass.PI".
/// </summary>
public const double PI = 3.14;
/// <summary>
/// Enter description for method func.
/// ID string generated is "M:MyNamespace.MyClass.func".
/// </summary>
/// <returns>Describe return value.</returns>
public int func() { return 1; }
/// <summary>
/// Enter description for method someMethod.
/// ID string generated is
"M:MyNamespace.MyClass.someMethod(System.String,System.Int32@,System.Void*)".
/// </summary>
/// <param name="str">Describe parameter.</param>
/// <param name="num">Describe parameter.</param>
/// <param name="ptr">Describe parameter.</param>
/// <returns>Describe return value.</returns>
public int someMethod(string str, ref int nm, void* ptr) { return 1; }
/// <summary>
/// Enter description for method anotherMethod.
/// ID string generated is
"M:MyNamespace.MyClass.anotherMethod(System.Int16[],System.Int32[0:,0:])".
/// </summary>
/// <param name="array1">Describe parameter.</param>
/// <param name="array">Describe parameter.</param>
/// <returns>Describe return value.</returns>
public int anotherMethod(short[] array1, int[,] array) { return 0; }
/// <summary>
/// Enter description for operator.
/// ID string generated is
"M:MyNamespace.MyClass.op_Addition(MyNamespace.MyClass,MyNamespace.MyClass)".
/// </summary>
/// <param name="first">Describe parameter.</param>
/// <param name="second">Describe parameter.</param>
/// <returns>Describe return value.</returns>
public static MyClass operator +(MyClass first, MyClass second) { return first; }
/// <summary>
/// Enter description for property.
/// ID string generated is "P:MyNamespace.MyClass.prop".
/// </summary>
public int prop { get { return 1; } set { } }
/// <summary>
/// Enter description for event.
/// ID string generated is "E:MyNamespace.MyClass.OnHappened".
/// </summary>
public event Del OnHappened;
/// <summary>
/// Enter description for index.
/// ID string generated is "P:MyNamespace.MyClass.Item(System.String)".
/// </summary>
/// </summary>
/// <param name="str">Describe parameter.</param>
/// <returns></returns>
public int this[string s] { get { return 1; } }
/// <summary>
/// Enter description for class Nested.
/// ID string generated is "T:MyNamespace.MyClass.Nested".
/// </summary>
public class Nested { }
/// <summary>
/// Enter description for delegate.
/// ID string generated is "T:MyNamespace.MyClass.Del".
/// </summary>
/// <param name="i">Describe parameter.</param>
public delegate void Del(int i);
/// <summary>
/// Enter description for operator.
/// ID string generated is "M:MyNamespace.MyClass.op_Explicit(MyNamespace.X)~System.Int32".
/// </summary>
/// <param name="myParameter">Describe parameter.</param>
/// <returns>Describe return value.</returns>
public static explicit operator int(MyClass myParameter) { return 1; }
}
}
NOTE
El archivo XML no proporciona información completa sobre los tipos y los miembros (por ejemplo, no
contiene información de tipos). Para obtener información completa sobre un tipo o miembro, use el
archivo de documentación con reflexión en el tipo o miembro reales.
Los desarrolladores pueden crear su propio conjunto de etiquetas, que el compilador copia en el archivo de
salida.
Algunas de las etiquetas recomendadas se pueden usar en cualquier elemento de lenguaje. Otras tienen un uso
más especializado. Por último, algunas de las etiquetas se usan para aplicar formato al texto de la
documentación. En este artículo se describen las etiquetas recomendadas organizadas por su uso.
El compilador comprueba la sintaxis de los elementos seguidos de un solo * de la lista siguiente. Visual Studio
proporciona a IntelliSense las etiquetas comprobadas por el compilador y todas las etiquetas seguidas de ** de
la lista siguiente. Además de las que aparecen aquí, el compilador y Visual Studio validan las etiquetas <b> ,
<i> , <u> , <br/> y <a> . El compilador también valida <tt> , que es HTML en desuso.
Etiquetas generales usadas para varios elementos: estas etiquetas son el conjunto mínimo de cualquier API.
<summary> : el valor de este elemento se muestra en IntelliSense en Visual Studio.
<remarks> **
Etiquetas usadas en miembros: estas etiquetas se usan al documentar métodos y propiedades.
<returns> : el valor de este elemento se muestra en IntelliSense en Visual Studio.
<param> *: el valor de este elemento se muestra en IntelliSense en Visual Studio.
<paramref>
<exception> *
<value> : el valor de este elemento se muestra en IntelliSense en Visual Studio.
Formato de salida de documentación: estas etiquetas proporcionan instrucciones de formato para las
herramientas que generan documentación.
<para>
<list>
<c>
<code>
<example> **
Reutilización de texto de documentación: estas etiquetas proporcionan herramientas que facilitan la
reutilización de comentarios XML.
<inheritdoc> **
<include> *
Generación de vínculos y referencias: estas etiquetas generan vínculos a otra documentación.
<see> *
<seealso> *
<cref>
<href>
Etiquetas para métodos y tipos genéricos: estas etiquetas solo se usan en métodos y tipos genéricos.
<typeparam> *: el valor de este elemento se muestra en IntelliSense en Visual Studio.
<typeparamref>
NOTE
Los comentarios de documentación no pueden aplicarse en un espacio de nombres.
Etiquetas generales
<summary>
<summary>description</summary>
La etiqueta <summary> debe usarse para describir un tipo o un miembro de tipo. Use <remarks> para agregar
información adicional a una descripción de tipo. Use el atributo cref para permitir que herramientas de
documentación como DocFX y Sandcastle creen hipervínculos internos a las páginas de documentación de los
elementos de código. El texto de la etiqueta <summary> es la única fuente de información sobre el tipo en
IntelliSense, y también se muestra en la ventana Examinador de objetos.
<remarks>
<remarks>
description
</remarks>
La etiqueta <remarks> se usa para agregar información sobre un tipo o miembro de este, y complementa la
información especificada con <summary>. Esta información se muestra en la ventana Examinador de objetos.
Esta etiqueta puede incluir explicaciones más extensas. Puede que le resulte más cómodo escribirla con
secciones CDATA para Markdown. Herramientas como docfx procesan el texto de Markdown en secciones
CDATA .
<returns>description</returns>
La etiqueta <returns> debe usarse en el comentario de una declaración de método para describir el valor
devuelto.
<param>
<param name="name">description</param>
name: nombre de un parámetro de método. Ponga el nombre entre comillas dobles (" "). Los nombres de los
parámetros deben coincidir con la firma de la API. Si uno o varios parámetros no aparecen, el compilador
emite una advertencia. El compilador también emite una advertencia si el valor de name no coincide con un
parámetro formal de la declaración del método.
La etiqueta <param> debe usarse en el comentario de una declaración de método para describir uno de los
parámetros del método. Para documentar varios parámetros, use varias etiquetas <param> . El texto de la
etiqueta <param> se muestra en IntelliSense, el examinador de objetos y el informe web de comentario de
código.
<paramref>
<paramref name="name"/>
name : nombre del parámetro al que se hace referencia. Ponga el nombre entre comillas dobles (" ").
La etiqueta <paramref> ofrece una manera de indicar que una palabra en los comentarios del código (por
ejemplo, en un bloque <summary> o <remarks> ) hace referencia a un parámetro. El archivo XML se puede
procesar para dar formato a esta palabra de alguna manera distinta, por ejemplo, con una fuente en negrita o
cursiva.
<exception>
<exception cref="member">description</exception>
cref = " member ": referencia a una excepción que está disponible desde el entorno de compilación actual. El
compilador comprueba si la excepción dada existe y traduce member al nombre de elemento canónico en la
salida XML. member debe aparecer entre comillas dobles (" ").
La etiqueta <exception> le permite especificar qué excepciones se pueden producir. Esta etiqueta se puede
aplicar a definiciones de métodos, propiedades, eventos e indizadores.
<value>
<value>property-description</value>
La etiqueta <value> le permite describir el valor que representa una propiedad. Cuando agrega una propiedad
mediante un asistente de código en el entorno de desarrollo .NET de Visual Studio, agregará una etiqueta
<summary> para la nueva propiedad. Debe agregar manualmente una etiqueta <value> para describir el valor
que representa la propiedad.
<remarks>
<para>
This is an introductory paragraph.
</para>
<para>
This paragraph contains more details.
</para>
</remarks>
La etiqueta <para> se usa dentro de otra etiqueta, como <summary>, <remarks> o <returns>, y permite dar
una estructura al texto. La etiqueta <para> crea un párrafo a doble espacio. Use la etiqueta <br/> si quiere un
párrafo a un solo espacio.
<list>
<list type="bullet|number|table">
<listheader>
<term>term</term>
<description>description</description>
</listheader>
<item>
<term>Assembly</term>
<description>The library or executable built from a compilation.</description>
</item>
</list>
El bloque <listheader> se usa para definir la fila de encabezado de una tabla o de una lista de definiciones.
Cuando se define una tabla, solo es necesario proporcionar una entrada para term en el encabezado. Cada
elemento de la lista se especifica con un bloque <item> . Cuando se crea una lista de definiciones, es necesario
especificar tanto term como description . En cambio, para una tabla, lista con viñetas o lista numerada, solo es
necesario suministrar una entrada para description . Una lista o una tabla pueden tener tantos bloques <item>
como sean necesarios.
<c>
<c>text</c>
La etiqueta <c> le proporciona una manera de indicar que el texto dentro de una descripción debe marcarse
como código. Use <code> para indicar varias líneas como código.
<code>
<code>
var index = 5;
index++;
</code>
La etiqueta <code> se usa para indicar varias líneas de código. Use <c> para indicar que el texto de línea única
dentro de una descripción debe marcarse como código.
<example>
<example>
This shows how to increment an integer.
<code>
var index = 5;
index++;
</code>
</example>
La etiqueta <example> le permite especificar un ejemplo de cómo usar un método u otro miembro de biblioteca.
Un ejemplo normalmente implica el uso de la etiqueta <code>.
Herede comentarios XML de clases base, interfaces y métodos similares. El empleo de inheritdoc acaba con el
copiado y pegado no deseados de comentarios XML duplicados y mantiene los comentarios XML sincronizados
automáticamente. Tenga en cuenta que al agregar la etiqueta <inheritdoc> a un tipo, todos los miembros
heredan también los comentarios.
cref : especifica el miembro del que se hereda la documentación. Las etiquetas ya definidas en el miembro
actual no las invalidan las heredadas.
path : consulta de expresión XPath que genera un conjunto de nodos para mostrar. Puede usar este atributo
para filtrar las etiquetas que se van a incluir o excluir en la documentación heredada.
Agregue los comentarios XML en las clases base o las interfaces y deje que inheritdoc copie los comentarios en
las clases en implementación. Agregue los comentarios XML a los métodos sincrónicos y deje que inheritdoc
copie los comentarios en las versiones asincrónicas de los mismos métodos. Si quiere copiar los comentarios de
un miembro concreto, puede usar el atributo cref para especificar el miembro.
<include>
filename : nombre del archivo XML que contiene la documentación. El nombre de archivo se puede calificar
con una ruta de acceso relativa al archivo de código fuente. Incluya filename entre comillas simples (' ').
tagpath : ruta de acceso de las etiquetas de filename que conduce a la etiqueta name . Incluya la ruta de
acceso entre comillas simples (' ').
name : especificador de nombre de la etiqueta que precede a los comentarios; name tiene un elemento id .
id : identificador de la etiqueta que precede a los comentarios. Ponga el identificador entre comillas dobles
(" ").
La etiqueta <include> le permite hacer referencia a comentarios colocados en otro archivo que describen los
tipos y miembros en el código fuente. La inclusión de un archivo externo es una alternativa a la colocación de
los comentarios de la documentación directamente en el archivo de código fuente. Al colocar la documentación
en un archivo independiente, puede aplicar el control de código fuente a la documentación de forma
independiente desde el código fuente. Una persona puede haber extraído del repositorio el archivo de código
fuente y otra el archivo de documentación. La etiqueta <include> usa la sintaxis XML de XPath. Consulte la
documentación de XPath para ver formas de personalizar el uso de <include> .
cref="member" : referencia a un miembro o campo al cual se puede llamar desde el entorno de compilación
actual. El compilador comprueba si el elemento de código dado existe y pasa member al nombre de elemento
en el resultado XML. Agregue member entre comillas dobles (" "). Puede proporcionar otro texto de vínculo
para "cref" mediante una etiqueta de cierre independiente.
href="link" : vínculo interactivo a una dirección URL determinada. Por ejemplo,
<see href="https://github.com">GitHub</see> genera un vínculo en el que se puede hacer clic, con texto
GitHub que vincula a https://github.com .
langword="keyword" : palabra clave de lenguaje, como true .
La etiqueta <see> permite especificar un vínculo desde el texto. Use <seealso> para indicar que el texto debe
colocarse en una sección Vea también. Use el atributo cref para crear hipervínculos internos a las páginas de
documentación de los elementos de código. Incluya los parámetros de tipo para especificar una referencia a un
tipo genérico o método, como cref="cref="IDictionary{T, U}" . Además, href es un atributo válido que actúa
como un hipervínculo.
<seealso>
cref="member" : referencia a un miembro o campo al cual se puede llamar desde el entorno de compilación
actual. El compilador comprueba si el elemento de código dado existe y pasa member al nombre de elemento
en el resultado XML. member debe aparecer entre comillas dobles (" ").
href="link" : vínculo interactivo a una dirección URL determinada. Por ejemplo,
<seealso href="https://github.com">GitHub</seealso> genera un vínculo en el que se puede hacer clic, con
texto GitHub que vincula a https://github.com .
La etiqueta <seealso> permite especificar el texto que quiere que aparezca en una sección Vea también . Use
<see> para especificar un vínculo desde dentro del texto. No se puede anidar la etiqueta seealso dentro de la
etiqueta summary .
Atributo cref
El atributo cref en una etiqueta de documentación XML significa "referencia de código". Especifica que el texto
interno de la etiqueta es un elemento de código, como un tipo, un método o una propiedad. En herramientas de
documentación como DocFX y Sandcastle, use los atributos cref para generar hipervínculos a la página donde
se documenta el tipo o miembro de manera automática.
Atributo href
El atributo href significa una referencia a una página web. Puede usarlo para hacer referencia directamente a la
documentación en línea sobre la API o la biblioteca.
TResult : nombre del parámetro de tipo. Ponga el nombre entre comillas dobles (" ").
La etiqueta <typeparam> debe usarse en el comentario de una declaración de método o tipo genérico para
describir un parámetro de tipo. Agregue una etiqueta para cada parámetro de tipo del tipo o método genérico.
El texto de la etiqueta <typeparam> se muestra en IntelliSense.
<typeparamref>
<typeparamref name="TKey"/>
TKey : nombre del parámetro de tipo. Ponga el nombre entre comillas dobles (" ").
Use esta etiqueta para permitir que los consumidores del archivo de documentación den formato a la palabra
de alguna manera distinta, por ejemplo en cursiva.
Etiquetas definidas por el usuario
Todas las etiquetas descritas anteriormente son las que reconoce el compilador de C#, pero el usuario puede
definir sus propias etiquetas. Herramientas como Sandcastle proporcionan compatibilidad con etiquetas
adicionales como <event> y <note>, e incluso permiten documentar espacios de nombres. También se pueden
usar herramientas de generación de documentación internas o personalizadas con las etiquetas estándar, y se
admiten varios formatos de salida, de HTML a PDF.
Comentarios de documentación XML de ejemplo
16/09/2021 • 18 minutes to read
Este artículo contiene tres ejemplos para agregar comentarios de documentación XML a la mayoría de los
elementos de lenguaje de C#. En el primer ejemplo se muestra cómo documentar una clase con miembros
diferentes. En el segundo se muestra cómo reutilizar las explicaciones de una jerarquía de clases o interfaces. En
el tercero se muestran las etiquetas que se van a usar para las clases y los miembros genéricos. En el segundo y
tercer ejemplo se usan conceptos que se tratan en el primero.
/// <summary>
/// Every class and member should have a one sentence
/// summary describing its purpose.
/// </summary>
/// <remarks>
/// You can expand on that one sentence summary to
/// provide more information for readers. In this case,
/// the <c>ExampleClass</c> provides different C#
/// elements to show how you would add documentation
///comments for most elements in a typical class.
/// <para>
/// The remarks can add multiple paragraphs, so you can
/// write detailed information for developers that use
/// your work. You should add everything needed for
/// readers to be successful. This class contains
/// examples for the following:
/// </para>
/// <list type="table">
/// <item>
/// <term>Summary</term>
/// <description>
/// This should provide a one sentence summary of the class or member.
/// </description>
/// </item>
/// <item>
/// <term>Remarks</term>
/// <description>
/// This is typically a more detailed description of the class or member
/// </description>
/// </item>
/// <item>
/// <term>para</term>
/// <description>
/// The para tag separates a section into multiple paragraphs
/// </description>
/// </item>
/// <item>
/// <term>list</term>
/// <description>
/// Provides a list of terms or elements
/// </description>
/// </item>
/// <item>
/// <term>returns, param</term>
/// <description>
/// <description>
/// Used to describe parameters and return values
/// </description>
/// </item>
/// <item>
/// <term>value</term>
/// <description>Used to describe properties</description>
/// </item>
/// <item>
/// <term>exception</term>
/// <description>
/// Used to describe exceptions that may be thrown
/// </description>
/// </item>
/// <item>
/// <term>c, cref, see, seealso</term>
/// <description>
/// These provide code style and links to other
/// documentation elements
/// </description>
/// </item>
/// <item>
/// <term>example, code</term>
/// <description>
/// These are used for code examples
/// </description>
/// </item>
/// </list>
/// <para>
/// The list above uses the "table" style. You could
/// also use the "bullet" or "number" style. Neither
/// would typically use the "term" element.
/// <br/>
/// Note: paragraphs are double spaced. Use the *br*
/// tag for single spaced lines.
/// </para>
/// </remarks>
public class ExampleClass
{
/// <value>
/// The <c>Label</c> property represents a label
/// for this instance.
/// </value>
/// <remarks>
/// The <see cref="Label"/> is a <see langword="string"/>
/// that you use for a label.
/// <para>
/// Note that there isn't a way to provide a "cref" to
/// each accessor, only to the property itself.
/// </para>
/// </remarks>
public string Label
{
get;
set;
}
/// <summary>
/// Adds two integers and returns the result.
/// </summary>
/// <returns>
/// The sum of two integers.
/// </returns>
/// <param name="left">
/// The left operand of the addition.
/// </param>
/// <param name="right">
/// The right operand of the addition.
/// </param>
/// <example>
/// <code>
/// int c = Math.Add(4, 5);
/// if (c > 10)
/// {
/// Console.WriteLine(c);
/// }
/// </code>
/// </example>
/// <exception cref="System.OverflowException">
/// Thrown when one parameter is
/// <see cref="Int32.MaxValue">MaxValue</see> and the other is
/// greater than 0.
/// Note that here you can also use
/// <see href="https://docs.microsoft.com/dotnet/api/system.int32.maxvalue"/>
/// to point a web page instead.
/// </exception>
/// <see cref="ExampleClass"/> for a list of all
/// the tags in these examples.
/// <seealso cref="ExampleClass.Label"/>
public static int Add(int left, int right)
{
if ((left == int.MaxValue && right > 0) || (right == int.MaxValue && left > 0))
throw new System.OverflowException();
/// <summary>
/// This is an example of a positional record.
/// </summary>
/// <remarks>
/// There isn't a way to add XML comments for properties
/// created for positional records, yet. The language
/// design team is still considering what tags should
/// be supported, and where. Currently, you can use
/// the "param" tag to describe the parameters to the
/// primary constructor.
/// </remarks>
/// <param name="FirstName">
/// This tag will apply to the primary constructor parameter.
/// </param>
/// <param name="LastName">
/// This tag will apply to the primary constructor parameter.
/// </param>
public record Person(string FirstName, string LastName);
}
La incorporación de documentación puede abarrotar el código fuente con grandes conjuntos de comentarios
destinados a los usuarios de la biblioteca. Use la etiqueta <Include> para separar los comentarios XML del
código fuente. El código fuente hace referencia a un archivo XML con la etiqueta <Include> :
/// <include file='xml_include_tag.xml' path='MyDocs/MyMembers[@name="test"]/*' />
class Test
{
static void Main()
{
}
}
<MyDocs>
<MyMembers name="test">
<summary>
The summary for this type.
</summary>
</MyMembers>
<MyMembers name="test2">
<summary>
The summary for this other type.
</summary>
</MyMembers>
</MyDocs>
/// <summary>
/// A summary about this class.
/// </summary>
/// <remarks>
/// These remarks would explain more about this class.
/// In this example, these comments also explain the
/// general information about the derived class.
/// </remarks>
public class MainClass
{
}
///<inheritdoc/>
public class DerivedClass : MainClass
{
}
/// <summary>
/// This interface would describe all the methods in
/// its contract.
/// </summary>
/// <remarks>
/// <remarks>
/// While elided for brevity, each method or property
/// in this interface would contain docs that you want
/// to duplicate in each implementing class.
/// </remarks>
public interface ITestInterface
{
/// <summary>
/// This method is part of the test interface.
/// </summary>
/// <remarks>
/// This content would be inherited by classes
/// that implement this interface when the
/// implementing class uses "inheritdoc"
/// </remarks>
/// <returns>The value of <paramref name="arg" /> </returns>
/// <param name="arg">The argument to the method</param>
int Method(int arg);
}
///<inheritdoc cref="ITestInterface"/>
public class ImplementingClass : ITestInterface
{
// doc comments are inherited here.
public int Method(int arg) => arg;
}
/// <summary>
/// This class shows hows you can "inherit" the doc
/// comments from one method in another method.
/// </summary>
/// <remarks>
/// You can inherit all comments, or only a specific tag,
/// represented by an xpath expression.
/// </remarks>
public class InheritOnlyReturns
{
/// <summary>
/// In this example, this summary is only visible for this method.
/// </summary>
/// <returns>A boolean</returns>
public static bool MyParentMethod(bool x) { return x; }
/// <Summary>
/// This class shows an example ofsharing comments across methods.
/// </Summary>
public class InheritAllButRemarks
{
/// <summary>
/// In this example, this summary is visible on all the methods.
/// </summary>
/// <remarks>
/// The remarks can be inherited by other methods
/// using the xpath expression.
/// </remarks>
/// <returns>A boolean</returns>
public static bool MyParentMethod(bool x) { return x; }
Tipos genéricos
Use la etiqueta <typeparam> para describir los parámetros de tipo de tipos y métodos genéricos. El valor del
atributo cref exige nueva sintaxis para hacer referencia a una clase o un método genéricos:
/// <summary>
/// This is a generic class.
/// </summary>
/// <remarks>
/// This example shows how to specify the <see cref="GenericClass{T}"/>
/// type as a cref attribute.
/// In generic classes and methods, you'll often want to reference the
/// generic type, or the type parameter.
/// </remarks>
class GenericClass<T>
{
// Fields and members.
}
/// <Summary>
/// This shows examples of typeparamref and typeparam tags
/// </Summary>
public class ParamsAndParamRefs
{
/// <summary>
/// The GetGenericValue method.
/// </summary>
/// <remarks>
/// This sample shows how to specify the <see cref="GetGenericValue"/>
/// method as a cref attribute.
/// The parameter and return value are both of an arbitrary type,
/// <typeparamref name="T"/>
/// </remarks>
public static T GetGenericValue<T>(T para)
{
return para;
}
}
namespace TaggedLibrary
{
/*
The main Math class
Contains all methods for performing basic math functions
*/
/// <summary>
/// The main <c>Math</c> class.
/// Contains all methods for performing basic math functions.
/// <list type="bullet">
/// <item>
/// <term>Add</term>
/// <description>Addition Operation</description>
/// </item>
/// <item>
/// <term>Subtract</term>
/// <description>Subtraction Operation</description>
/// </item>
/// <item>
/// <term>Multiply</term>
/// <description>Multiplication Operation</description>
/// </item>
/// <item>
/// <item>
/// <term>Divide</term>
/// <description>Division Operation</description>
/// </item>
/// </list>
/// </summary>
/// <remarks>
/// <para>
/// This class can add, subtract, multiply and divide.
/// </para>
/// <para>
/// These operations can be performed on both
/// integers and doubles.
/// </para>
/// </remarks>
public class Math
{
// Adds two integers and returns the result
/// <summary>
/// Adds two integers <paramref name="a"/> and <paramref name="b"/>
/// and returns the result.
/// </summary>
/// <returns>
/// The sum of two integers.
/// </returns>
/// <example>
/// <code>
/// int c = Math.Add(4, 5);
/// if (c > 10)
/// {
/// Console.WriteLine(c);
/// }
/// </code>
/// </example>
/// <exception cref="System.OverflowException">
/// Thrown when one parameter is <see cref="Int32.MaxValue"/> and the other
/// is greater than 0.
/// </exception>
/// See <see cref="Math.Add(double, double)"/> to add doubles.
/// <seealso cref="Math.Subtract(int, int)"/>
/// <seealso cref="Math.Multiply(int, int)"/>
/// <seealso cref="Math.Divide(int, int)"/>
/// <param name="a">An integer.</param>
/// <param name="b">An integer.</param>
public static int Add(int a, int b)
{
// If any parameter is equal to the max value of an integer
// and the other is greater than zero
if ((a == int.MaxValue && b > 0) ||
(b == int.MaxValue && a > 0))
{
throw new System.OverflowException();
}
return a + b;
}
return a + b;
}
Es posible que los comentarios oculten el código. En el ejemplo final se muestra cómo se adaptaría esta
biblioteca para usar la etiqueta include . Mueva toda la documentación a un archivo XML:
<docs>
<members name="math">
<Math>
<summary>
The main <c>Math</c> class.
Contains all methods for performing basic math functions.
</summary>
<remarks>
<para>This class can add, subtract, multiply and divide.</para>
<para>These operations can be performed on both integers and doubles.</para>
</remarks>
</Math>
<AddInt>
<summary>
Adds two integers <paramref name="a"/> and <paramref name="b"/>
and returns the result.
</summary>
<returns>
The sum of two integers.
</returns>
<example>
<code>
int c = Math.Add(4, 5);
if (c > 10)
{
Console.WriteLine(c);
}
</code>
</example>
<exception cref="System.OverflowException">Thrown when one
parameter is max
and the other is greater than 0.</exception>
See <see cref="Math.Add(double, double)"/> to add doubles.
<seealso cref="Math.Subtract(int, int)"/>
<seealso cref="Math.Multiply(int, int)"/>
<seealso cref="Math.Divide(int, int)"/>
<param name="a">An integer.</param>
<param name="b">An integer.</param>
</AddInt>
<AddDouble>
<summary>
Adds two doubles <paramref name="a"/> and <paramref name="b"/>
and returns the result.
</summary>
<returns>
The sum of two doubles.
</returns>
<example>
<code>
double c = Math.Add(4.5, 5.4);
if (c > 10)
{
Console.WriteLine(c);
}
</code>
</example>
<exception cref="System.OverflowException">Thrown when one parameter is max
and the other is greater than 0.</exception>
See <see cref="Math.Add(int, int)"/> to add integers.
<seealso cref="Math.Subtract(double, double)"/>
<seealso cref="Math.Multiply(double, double)"/>
<seealso cref="Math.Divide(double, double)"/>
<param name="a">A double precision number.</param>
<param name="b">A double precision number.</param>
</AddDouble>
<SubtractInt>
<summary>
Subtracts <paramref name="b"/> from <paramref name="a"/> and
returns the result.
returns the result.
</summary>
<returns>
The difference between two integers.
</returns>
<example>
<code>
int c = Math.Subtract(4, 5);
if (c > 1)
{
Console.WriteLine(c);
}
</code>
</example>
See <see cref="Math.Subtract(double, double)"/> to subtract doubles.
<seealso cref="Math.Add(int, int)"/>
<seealso cref="Math.Multiply(int, int)"/>
<seealso cref="Math.Divide(int, int)"/>
<param name="a">An integer.</param>
<param name="b">An integer.</param>
</SubtractInt>
<SubtractDouble>
<summary>
Subtracts a double <paramref name="b"/> from another
double <paramref name="a"/> and returns the result.
</summary>
<returns>
The difference between two doubles.
</returns>
<example>
<code>
double c = Math.Subtract(4.5, 5.4);
if (c > 1)
{
Console.WriteLine(c);
}
</code>
</example>
See <see cref="Math.Subtract(int, int)"/> to subtract integers.
<seealso cref="Math.Add(double, double)"/>
<seealso cref="Math.Multiply(double, double)"/>
<seealso cref="Math.Divide(double, double)"/>
<param name="a">A double precision number.</param>
<param name="b">A double precision number.</param>
</SubtractDouble>
<MultiplyInt>
<summary>
Multiplies two integers <paramref name="a"/> and
<paramref name="b"/> and returns the result.
</summary>
<returns>
The product of two integers.
</returns>
<example>
<code>
int c = Math.Multiply(4, 5);
if (c > 100)
{
Console.WriteLine(c);
}
</code>
</example>
See <see cref="Math.Multiply(double, double)"/> to multiply doubles.
<seealso cref="Math.Add(int, int)"/>
<seealso cref="Math.Subtract(int, int)"/>
<seealso cref="Math.Divide(int, int)"/>
<param name="a">An integer.</param>
<param name="b">An integer.</param>
</MultiplyInt>
<MultiplyDouble>
<summary>
Multiplies two doubles <paramref name="a"/> and
<paramref name="b"/> and returns the result.
</summary>
<returns>
The product of two doubles.
</returns>
<example>
<code>
double c = Math.Multiply(4.5, 5.4);
if (c > 100.0)
{
Console.WriteLine(c);
}
</code>
</example>
See <see cref="Math.Multiply(int, int)"/> to multiply integers.
<seealso cref="Math.Add(double, double)"/>
<seealso cref="Math.Subtract(double, double)"/>
<seealso cref="Math.Divide(double, double)"/>
<param name="a">A double precision number.</param>
<param name="b">A double precision number.</param>
</MultiplyDouble>
<DivideInt>
<summary>
Divides an integer <paramref name="a"/> by another integer
<paramref name="b"/> and returns the result.
</summary>
<returns>
The quotient of two integers.
</returns>
<example>
<code>
int c = Math.Divide(4, 5);
if (c > 1)
{
Console.WriteLine(c);
}
</code>
</example>
<exception cref="System.DivideByZeroException">
Thrown when <paramref name="b"/> is equal to 0.
</exception>
See <see cref="Math.Divide(double, double)"/> to divide doubles.
<seealso cref="Math.Add(int, int)"/>
<seealso cref="Math.Subtract(int, int)"/>
<seealso cref="Math.Multiply(int, int)"/>
<param name="a">An integer dividend.</param>
<param name="b">An integer divisor.</param>
</DivideInt>
<DivideDouble>
<summary>
Divides a double <paramref name="a"/> by another
double <paramref name="b"/> and returns the result.
</summary>
<returns>
The quotient of two doubles.
</returns>
<example>
<code>
double c = Math.Divide(4.5, 5.4);
if (c > 1.0)
{
Console.WriteLine(c);
}
</code>
</example>
<exception cref="System.DivideByZeroException">Thrown when <paramref name="b"/> is equal to 0.
</exception>
See <see cref="Math.Divide(int, int)"/> to divide integers.
<seealso cref="Math.Add(double, double)"/>
<seealso cref="Math.Subtract(double, double)"/>
<seealso cref="Math.Multiply(double, double)"/>
<param name="a">A double precision dividend.</param>
<param name="b">A double precision divisor.</param>
</DivideDouble>
</members>
</docs>
En el XML anterior, los comentarios de documentación de cada miembro aparecen directamente dentro de una
etiqueta cuyo nombre indica lo que hacen, pero puede elegir su propia estrategia. El código usa la etiqueta
<include> para hacer referencia al elemento adecuado del archivo XML:
namespace IncludeTag
{
/*
The main Math class
Contains all methods for performing basic math functions
*/
/// <include file='include.xml' path='docs/members[@name="math"]/Math/*'/>
public class Math
{
// Adds two integers and returns the result
/// <include file='include.xml' path='docs/members[@name="math"]/AddInt/*'/>
public static int Add(int a, int b)
{
// If any parameter is equal to the max value of an integer
// and the other is greater than zero
if ((a == int.MaxValue && b > 0) || (b == int.MaxValue && a > 0))
throw new System.OverflowException();
return a + b;
}
return a + b;
}
El atributo file representa el nombre del archivo XML que contiene la documentación.
El atributo path representa una consulta XPath para el tag name presente en el file especificado.
El atributo name representa el especificador de nombre en la etiqueta que precede a los comentarios.
El atributo id , que se puede usar en lugar de name , representa el identificador de la etiqueta que precede a
los comentarios.
Errores del compilador de C#
16/09/2021 • 2 minutes to read
Algunos errores del compilador de C# tienen temas correspondientes que explican por qué se genera el error y,
en algunos casos, cómo corregirlo. Utilice uno de los siguientes pasos para ver si la Ayuda está disponible para
un mensaje de error concreto.
Si usa Visual Studio, seleccione el número de error (por ejemplo, CS0029) en la Ventana de salida y después
presione la tecla F1.
Escriba el número de error en el cuadro Filtrar por título de la tabla de contenido.
Si ninguno de estos pasos conduce a información sobre el error, vaya al final de esta página y envíe comentarios
con el número o el texto del error.
Para obtener información sobre cómo configurar las opciones de advertencia y error en C#, vea Opciones del
compilador de C# o Compilar (Página, Diseñador de proyectos) (C#) en Visual Studio.
NOTE
Es posible que el equipo muestre nombres o ubicaciones diferentes para algunos de los elementos de la interfaz de
usuario de Visual Studio en las siguientes instrucciones. La edición de Visual Studio que se tenga y la configuración que se
utilice determinan estos elementos. Para obtener más información, vea Personalizar el IDE.
Vea también
Opciones del compilador de C#
Página Compilar (Diseñador de proyectos) (C#)
WarningLevel (opciones del compilador de C#)
DisabledWarnings (opciones del compilador de C#)
Introducción
18/09/2021 • 105 minutes to read
C# (pronunciado "si sharp" en inglés) es un lenguaje de programación sencillo, moderno, orientado a objetos y
con seguridad de tipos. C# tiene sus raíces en la familia de lenguajes C y será inmediatamente familiar para los
programadores de C, C++ y Java. C# está normalizado por ECMA International como el estándar *ECMA-334 _
y por ISO/IEC como el estándar _ *iso/IEC 23270**. El compilador de C# de Microsoft para el .NET Framework es
una implementación compatible de ambos estándares.
C# es un lenguaje orientado a objetos, pero también incluye compatibilidad para programación orientada a
componentes . El diseño de software contemporáneo se basa cada vez más en componentes de software en
forma de paquetes independientes y autodescriptivos de funcionalidad. La clave de estos componentes es que
presentan un modelo de programación con propiedades, métodos y eventos; tienen atributos que proporcionan
información declarativa sobre el componente; e incorporan su propia documentación. C# proporciona
construcciones de lenguaje para admitir directamente estos conceptos, lo que permite que C# sea un lenguaje
muy natural en el que crear y usar componentes de software.
Varias características de C# ayudan en la construcción de aplicaciones sólidas y duraderas: * la recolección de
elementos no utilizados _ recupera automáticamente la memoria ocupada por objetos no usados. el control de
excepciones proporciona un enfoque estructurado y extensible para la detección y recuperación de errores. y el
diseño con seguridad de tipos* del lenguaje hace imposible leer desde variables no inicializadas, indizar
matrices más allá de sus límites o realizar conversiones de tipo no comprobadas.
C# tiene un sistema de tipo unificado . Todos los tipos de C#, incluidos los tipos primitivos como int y
double , se heredan de un único tipo object raíz. Por lo tanto, todos los tipos comparten un conjunto de
operaciones comunes, y los valores de todos los tipos se pueden almacenar, transportar y utilizar de manera
coherente. Además, C# admite tipos de valor y tipos de referencia definidos por el usuario, lo que permite la
asignación dinámica de objetos, así como almacenamiento en línea de estructuras ligeras.
Para asegurarse de que las programas y las bibliotecas de C# pueden evolucionar a lo largo del tiempo de
manera compatible, se ha puesto mucho énfasis en el versionamiento del diseño de C#. Muchos lenguajes de
programación prestan poca atención a este problema y, como resultado, los programas escritos en dichos
lenguajes se interrumpen con más frecuencia de lo necesario cuando se introducen nuevas versiones de las
bibliotecas dependientes. Los aspectos del diseño de C# afectados directamente por las consideraciones de
versionamiento incluyen los modificadores virtual y override independientes, las reglas para la resolución
de sobrecargas de métodos y la compatibilidad para declaraciones explícitas de miembros de interfaz.
En el resto de este capítulo se describen las características esenciales del lenguaje C#. Aunque los capítulos
posteriores describen las reglas y las excepciones en un modo orientado a los detalles y, a veces, de forma
matemática, en este capítulo se realiza una mayor claridad y una brevedad a costa de la integridad. La intención
es proporcionar al lector una introducción al lenguaje que facilitará la escritura de programas iniciales y la
lectura de capítulos posteriores.
Hola a todos
El programa "Hola mundo" tradicionalmente se usa para presentar un lenguaje de programación. En este caso,
se usa C#:
using System;
class Hello
{
static void Main() {
Console.WriteLine("Hello, World");
}
}
Normalmente, los archivos de código fuente de C# tienen la extensión de archivo .cs . Suponiendo que el
programa "Hello, World" se almacena en el archivo hello.cs , el programa se puede compilar con el
compilador de Microsoft C# mediante la línea de comandos.
csc hello.cs
que genera un ensamblado ejecutable denominado hello.exe . La salida generada por esta aplicación cuando
se ejecuta es
Hello, World
El programa "Hola mundo" empieza con una directiva using que hace referencia al espacio de nombres
System . Los espacios de nombres proporcionan un método jerárquico para organizar las bibliotecas y los
programas de C#. Los espacios de nombres contienen tipos y otros espacios de nombres; por ejemplo, el
espacio de nombres System contiene varios tipos, como la clase Console a la que se hace referencia en el
programa, y otros espacios de nombres, como IO y Collections . Una directiva using que hace referencia a
un espacio de nombres determinado permite el uso no calificado de los tipos que son miembros de ese espacio
de nombres. Debido a la directiva using , puede utilizar el programa Console.WriteLine como abreviatura de
System.Console.WriteLine .
La clase Hello declarada por el programa "Hola mundo" tiene un miembro único, el método llamado Main . El
método Main se declara con el modificador static . Mientras que los métodos de instancia pueden hacer
referencia a una instancia de objeto envolvente determinada utilizando la palabra clave this , los métodos
estáticos funcionan sin referencia a un objeto determinado. Por convención, un método estático denominado
Main sirve como punto de entrada de un programa.
La salida del programa la genera el método WriteLine de la clase Console en el espacio de nombres System .
Esta clase la proporcionan las bibliotecas de clases de .NET Framework, a las que, de forma predeterminada, el
compilador de Microsoft C# hace referencia automáticamente. Tenga en cuenta que C# no tiene una biblioteca
en tiempo de ejecución independiente. En su lugar, el .NET Framework es la biblioteca en tiempo de ejecución de
C#.
namespace Acme.Collections
{
public class Stack
{
Entry top;
class Entry
{
public Entry next;
public object data;
declara una clase denominada Stack en un espacio de nombres denominado Acme.Collections . El nombre
completo de esta clase es Acme.Collections.Stack . La clase contiene varios miembros: un campo denominado
top , dos métodos denominados Push y Pop , y una clase anidada denominada Entry . La clase Entry
contiene además tres miembros: un campo denominado next , un campo denominado data y un constructor.
Suponiendo que el código fuente del ejemplo se almacene en el archivo acme.cs , la línea de comandos
compila el ejemplo como una biblioteca (código sin un punto de entrada Main y genera un ensamblado
denominado acme.dll .
Los ensamblados contienen código ejecutable en forma de instrucciones de lenguaje intermedio _ (IL) e
información simbólica con el formato _ Metadata *. Antes de ejecutarlo, el código de IL de un ensamblado se
convierte automáticamente en código específico del procesador mediante el compilador de Just in Time (JIT) de
.NET Common Language Runtime.
Dado que un ensamblado es una unidad autodescriptiva de funcionalidad que contiene código y metadatos, no
hay necesidad de directivas #include ni archivos de encabezado de C#. Los tipos y miembros públicos
contenidos en un ensamblado determinado estarán disponibles en un programa de C# simplemente haciendo
referencia a dicho ensamblado al compilar el programa. Por ejemplo, este programa usa la clase
Acme.Collections.Stack desde el ensamblado acme.dll :
using System;
using Acme.Collections;
class Test
{
static void Main() {
Stack s = new Stack();
s.Push(1);
s.Push(10);
s.Push(100);
Console.WriteLine(s.Pop());
Console.WriteLine(s.Pop());
Console.WriteLine(s.Pop());
}
}
Si el programa se almacena en el archivo test.cs , cuando test.cs se compila, se acme.dll puede hacer
referencia al ensamblado mediante la opción del compilador /r :
Esto crea un ensamblado ejecutable denominado test.exe , que, cuando se ejecuta, produce el resultado:
100
10
1
C# permite que el texto de origen de un programa se almacene en varios archivos de origen. Cuando se compila
un programa de C# de varios archivos, todos los archivos de origen se procesan juntos y los archivos de origen
pueden hacerse referencia mutuamente de forma libre; desde un punto de vista conceptual, es como si todos los
archivos de origen estuvieran concatenados en un archivo de gran tamaño antes de ser procesados. En C#
nunca se necesitan declaraciones adelantadas porque, excepto en contadas ocasiones, el orden de declaración es
insignificante. C# no limita un archivo de origen a declarar solamente un tipo público ni precisa que el nombre
del archivo de origen coincida con un tipo declarado en el archivo de origen.
Tipos y variables
Hay dos tipos de tipos en C#: *tipos de valor _ y _ tipos de referencia *. Las variables de tipos de valor
contienen directamente los datos, mientras que las variables de los tipos de referencia almacenan referencias a
los datos, lo que se conoce como objetos. Con los tipos de referencia, es posible que dos variables hagan
referencia al mismo objeto y que, por tanto, las operaciones en una variable afecten al objeto al que hace
referencia la otra variable. Con los tipos de valor, cada variable tiene su propia copia de los datos y no es posible
que las operaciones en una variable afecten a la otra (excepto en el caso de las variables de parámetro ref y
out ).
Los tipos de valor de C# se dividen en *** tipos simples*, _tipos de enumeración_, _tipos de struct_ y tipos que
_aceptan valores NULL_, y los tipos de referencia de C# se dividen en _tipos de clase_, tipos de _interfaz_, _tipos
de matriz*_ y tipos de delegado _ * * *.
En la tabla siguiente se proporciona información general sobre el sistema de tipos de C#.
Booleano: bool
Tipos que aceptan valores NULL Extensiones de todos los demás tipos
de valor con un valor null
Los ocho tipos enteros proporcionan compatibilidad con valores de 8, 16, 32 y 64 bits en formato con o sin
signo.
Los dos tipos de punto flotante, float y double , se representan mediante los formatos IEEE 754 de precisión
sencilla de 32 bits y de doble precisión de 64 bits.
El tipo decimal es un tipo de datos de 128 bits adecuado para cálculos financieros y monetarios.
El tipo de C# bool se usa para representar valores booleanos: valores que son true o false .
El procesamiento de caracteres y cadenas en C# utiliza la codificación Unicode. El tipo char representa una
unidad de código UTF-16 y el tipo string representa una secuencia de unidades de código UTF-16.
En la tabla siguiente se resumen los tipos numéricos de C#.
C AT EGO RÍA B IT S T IP O IN T ERVA LO / P REC ISIÓ N
64 long -9223372036854775808...
9, 223, 372, 036, 854, 775,
807
Los programas de C# utilizan declaraciones de tipos para crear nuevos tipos. Una declaración de tipos
especifica el nombre y los miembros del nuevo tipo. Cinco de las categorías de tipos de C# las define el usuario:
tipos de clase, tipos de estructura, tipos de interfaz, tipos de enumeración y tipos delegados.
Un tipo de clase define una estructura de datos que contiene miembros de datos (campos) y miembros de
función (métodos, propiedades, etc.). Los tipos de clase admiten herencia única y polimorfismo, mecanismos por
los que las clases derivadas pueden extender y especializar clases base.
Un tipo de estructura es similar a un tipo de clase en que representa una estructura con miembros de datos y
miembros de función. Sin embargo, a diferencia de las clases, las estructuras son tipos de valor y no requieren la
asignación del montón. Los tipos struct no admiten la herencia especificada por el usuario y todos los tipos de
struct se heredan implícitamente del tipo object .
Un tipo de interfaz define un contrato como un conjunto con nombre de miembros de función públicos. Una
clase o estructura que implementa una interfaz debe proporcionar implementaciones de los miembros de
función de la interfaz. Una interfaz puede heredar de varias interfaces base, y una clase o estructura puede
implementar varias interfaces.
Un tipo de delegado representa las referencias a métodos con una lista de parámetros determinada y un tipo de
valor devuelto. Los delegados permiten tratar métodos como entidades que se puedan asignar a variables y se
puedan pasar como parámetros. Los delegados son similares al concepto de punteros de función en otros
lenguajes, pero a diferencia de los punteros de función, los delegados están orientados a objetos y presentan
seguridad de tipos.
Los tipos de clase, estructura, interfaz y delegado admiten genéricos, mediante los cuales se pueden
parametrizar con otros tipos.
Un tipo de enumeración es un tipo distinto con constantes con nombre. Cada tipo de enumeración tiene un tipo
subyacente, que debe ser uno de los ocho tipos enteros. El conjunto de valores de un tipo de enumeración es
igual que el conjunto de valores del tipo subyacente.
C# admite matrices unidimensionales y multidimensionales de cualquier tipo. A diferencia de los tipos
enumerados anteriormente, los tipos de matriz no tienen que ser declarados antes de usarlos. En su lugar, los
tipos de matriz se crean mediante un nombre de tipo entre corchetes. Por ejemplo, int[] es una matriz
unidimensional de int , int[,] es una matriz bidimensional de int y es una matriz unidimensional de
int[][] Matrices unidimensionales de int .
Los tipos que aceptan valores NULL tampoco tienen que declararse antes de que se puedan utilizar. Para cada
tipo de valor que no acepta valores NULL T , existe un tipo que acepta valores NULL correspondiente T? , que
puede contener un valor adicional null . Por ejemplo, int? es un tipo que puede contener cualquier entero de
32 bits o el valor null .
El sistema de tipos de C# está unificado, de modo que un valor de cualquier tipo se puede tratar como un
objeto. Todos los tipos de C# directa o indirectamente se derivan del tipo de clase object , y object es la clase
base definitiva de todos los tipos. Los valores de tipos de referencia se tratan como objetos mediante la
visualización de los valores como tipo object . Los valores de los tipos de valor se tratan como objetos
mediante la realización de operaciones *Boxing _ y _ *conversión unboxing**. En el ejemplo siguiente, un valor
int se convierte en object y vuelve a int .
using System;
class Test
{
static void Main() {
int i = 123;
object o = i; // Boxing
int j = (int)o; // Unboxing
}
}
Cuando un valor de un tipo de valor se convierte al tipo object , se asigna una instancia de objeto, también
denominada "box", para contener el valor, y el valor se copia en ese cuadro. Por el contrario, cuando una object
referencia se convierte en un tipo de valor, se realiza una comprobación de que el objeto al que se hace
referencia es un cuadro del tipo de valor correcto y, si la comprobación se realiza correctamente, se copia el
valor en el cuadro.
El sistema de tipos unificado de C# significa que los tipos de valor pueden convertirse en objetos "a petición".
Debido a la unificación, las bibliotecas de uso general que utilizan el tipo object pueden usarse con tipos de
referencia y tipos de valor.
Hay varios tipos de variables en C#, entre otras, campos, elementos de matriz, variables locales y parámetros.
Las variables representan ubicaciones de almacenamiento, y cada variable tiene un tipo que determina qué
valores se pueden almacenar en la variable, como se muestra en la tabla siguiente.
Tipo de clase Una referencia nula, una referencia a una instancia de ese
tipo de clase o una referencia a una instancia de una clase
derivada de ese tipo de clase.
Tipo de matriz Una referencia nula, una referencia a una instancia de ese
tipo de matriz o una referencia a una instancia de un tipo de
matriz compatible
Tipo delegado Una referencia nula o una referencia a una instancia de ese
tipo de delegado.
Expresiones
Las expresiones _ se construyen a partir de _operandos_ y _operadores*. Los operadores de una expresión
indican qué operaciones se aplican a los operandos. Ejemplos de operadores incluyen + , - , _ , / y new .
Algunos ejemplos de operandos son literales, campos, variables locales y expresiones.
Cuando una expresión contiene varios operadores, el *precedencia _ de los operadores controla el orden en el
que se evalúan los operadores individuales. Por ejemplo, la expresión x + y _ z se evalúa como x + (y * z)
porque el operador * tiene mayor precedencia que el operador + .
La mayoría de los operadores se pueden sobrecargar . La sobrecarga de operador permite la especificación de
implementaciones de operadores definidas por el usuario para operaciones donde uno o ambos operandos son
de un tipo de struct o una clase definidos por el usuario.
En la tabla siguiente se resumen los operadores de C# y se enumeran las categorías de operador en orden de
prioridad, de mayor a menor. Los operadores de la misma categoría tienen la misma precedencia.
x++ Postincremento
x-- Postdecremento
Unario +x Identidad
-x Negación
!x Negación lógica
++x Preincremento
--x Predecremento
Multiplicativa x * y Multiplicación
x / y División
x % y Resto
Igualdad x == y Igual
x != y No igual a
Instrucciones
Las acciones de un programa se expresan mediante instrucciones . C# admite varios tipos de instrucciones
diferentes, varias de las cuales se definen en términos de instrucciones insertadas.
Un bloque permite que se escriban varias instrucciones en contextos donde se permite una única instrucción.
Un bloque se compone de una lista de instrucciones escritas entre los delimitadores { y } .
Las instrucciones de declaración se usan para declarar variables locales y constantes.
Las instrucciones de expresión se usan para evaluar expresiones. Entre las expresiones que se pueden
utilizar como instrucciones se incluyen las invocaciones de método, las asignaciones de objetos que usan el
new operador, las asignaciones mediante = y los operadores de asignación compuesta, las operaciones de
incremento y decremento mediante los ++ -- operadores y y las expresiones Await.
Las instrucciones de selección se usan para seleccionar una de varias instrucciones posibles para su
ejecución en función del valor de alguna expresión. En este grupo están las instrucciones if y switch .
Las instrucciones de iteración se utilizan para ejecutar repetidamente una instrucción incrustada. En este
grupo están las instrucciones while , do , for y foreach .
Las instrucciones de salto se usan para transferir el control. En este grupo están las instrucciones break ,
continue , goto , throw , return y yield .
La instrucción try ... catch se usa para detectar excepciones que se producen durante la ejecución de un
bloque, y la instrucción try ... finally se usa para especificar el código de finalización que siempre se ejecuta,
tanto si se ha producido una excepción como si no.
Las checked unchecked instrucciones y se utilizan para controlar el contexto de comprobación de
desbordamiento para conversiones y operaciones aritméticas de tipo entero.
La instrucción lock se usa para obtener el bloqueo de exclusión mutua para un objeto determinado, ejecutar
una instrucción y, luego, liberar el bloqueo.
La instrucción using se usa para obtener un recurso, ejecutar una instrucción y, luego, eliminar dicho recurso.
A continuación se muestran ejemplos de cada tipo de instrucción
Declaraciones de variables locales
Expression (Instrucción)
static void Main() {
int i;
i = 123; // Expression statement
Console.WriteLine(i); // Expression statement
i++; // Expression statement
Console.WriteLine(i); // Expression statement
}
Instrucción if
Instrucción switch
Instrucción while
Instrucción do
Instrucción for
static void Main(string[] args) {
for (int i = 0; i < args.Length; i++) {
Console.WriteLine(args[i]);
}
}
Instrucción foreach
Instrucción break
Instrucción continue
Instrucción goto
Instrucción return
Instrucción yield
static IEnumerable<int> Range(int from, int to) {
for (int i = from; i < to; i++) {
yield return i;
}
yield break;
}
throw``try instrucciones y
checked``unchecked instrucciones y
Instrucción lock
class Account
{
decimal balance;
public void Withdraw(decimal amount) {
lock (this) {
if (amount > balance) {
throw new Exception("Insufficient funds");
}
balance -= amount;
}
}
}
Instrucción using
Clases y objetos
*Las clases _ son los tipos más fundamentales de C#. Una clase es una estructura de datos que combina
estados (campos) y acciones (métodos y otros miembros de función) en una sola unidad. Una clase proporciona
una definición para las instancias creadas dinámicamente de la clase, también conocidas como objetos. Las
clases admiten la herencia y el polimorfismo, mecanismos por los que las clases derivadas pueden extender y
especializar _ clases base *.
Las clases nuevas se crean mediante declaraciones de clase. Una declaración de clase se inicia con un
encabezado que especifica los atributos y modificadores de la clase, el nombre de la clase, la clase base (si se
indica) y las interfaces implementadas por la clase. Al encabezado le sigue el cuerpo de la clase, que consta de
una lista de declaraciones de miembros escritas entre los delimitadores { y } .
La siguiente es una declaración de una clase simple denominada Point :
Las instancias de clases se crean mediante el operador new , que asigna memoria para una nueva instancia,
invoca un constructor para inicializar la instancia y devuelve una referencia a la instancia. Las instrucciones
siguientes crean dos objetos Point y almacenan las referencias en esos objetos en dos variables:
La memoria ocupada por un objeto se recupera automáticamente cuando el objeto ya no está en uso. No es
necesario ni posible desasignar explícitamente objetos en C#.
Miembros
Los miembros de una clase son *miembros estáticos _ o _ miembros de instancia *. Los miembros estáticos
pertenecen a clases y los miembros de instancia pertenecen a objetos (instancias de clases).
En la tabla siguiente se proporciona información general sobre los tipos de miembros que puede contener una
clase.
M EM B ER DESC RIP C IÓ N
Accesibilidad
Cada miembro de una clase tiene asociada una accesibilidad, que controla las regiones del texto del programa
que pueden tener acceso al miembro. Existen cinco formas posibles de accesibilidad. Estos se resumen en la
siguiente tabla.
Un tipo de clase que se declara para tomar parámetros de tipo se denomina tipo de clase genérico. Los tipos
struct, interfaz y delegado también pueden ser genéricos.
Cuando se usa la clase genérica, se deben proporcionar argumentos de tipo para cada uno de los parámetros de
tipo:
Un tipo genérico con argumentos de tipo proporcionados, como Pair<int,string> el anterior, se denomina tipo
construido.
Clases base
Una declaración de clase puede especificar una clase base colocando después del nombre de clase y los
parámetros de tipo dos puntos seguidos del nombre de la clase base. Omitir una especificación de la clase base
es igual que derivarla del tipo object . En el ejemplo siguiente, la clase base de Point3D es Point y la clase
base de Point es object :
Una clase hereda a los miembros de su clase base. La herencia significa que una clase contiene implícitamente
todos los miembros de su clase base, excepto la instancia y constructores estáticos, y los destructores de la clase
base. Una clase derivada puede agregar nuevos miembros a aquellos de los que hereda, pero no puede quitar la
definición de un miembro heredado. En el ejemplo anterior, Point3D hereda los campos x y y de Point y
cada instancia de Point3D contiene tres campos: x , y y z .
Existe una conversión implícita de un tipo de clase a cualquiera de sus tipos de clase base. Por lo tanto, una
variable de un tipo de clase puede hacer referencia a una instancia de esa clase o a una instancia de cualquier
clase derivada. Por ejemplo, dadas las declaraciones de clase anteriores, una variable de tipo Point puede hacer
referencia a una instancia de Point o Point3D :
Campos
Un campo es una variable que está asociada con una clase o a una instancia de una clase.
Un campo declarado con el static modificador define un campo estático . Un campo estático identifica
exactamente una ubicación de almacenamiento. Independientemente del número de instancias de una clase que
se creen, siempre solo hay una copia de un campo estático.
Un campo declarado sin el static modificador define un campo de instancia . Cada instancia de una clase
contiene una copia independiente de todos los campos de instancia de esa clase.
En el ejemplo siguiente, cada instancia de la clase Color tiene una copia independiente de los campos de
instancia r , g y b , pero solo hay una copia de los campos estáticos Black , White , Red , Green y Blue :
Como se muestra en el ejemplo anterior, los campos de solo lectura se puede declarar con un modificador
readonly . La asignación a un readonly campo solo se puede producir como parte de la declaración del campo
o en un constructor de la misma clase.
Métodos
Un *método _ es un miembro que implementa un cálculo o una acción que puede realizar un objeto o una
clase. Se tiene acceso a los métodos estáticos a través de la clase. _ Se tiene acceso a los métodos de instancia* a
través de instancias de la clase.
Los métodos tienen una lista (posiblemente vacía) de *Parameters _, que representan valores o referencias a
variables que se pasan al método, y un tipo de valor devuelto _ * * *, que especifica el tipo del valor calculado y
devuelto por el método. El tipo de valor devuelto de un método es void si no devuelve un valor.
Al igual que los tipos, los métodos también pueden tener un conjunto de parámetros de tipo, para lo cuales se
deben especificar argumentos de tipo cuando se llama al método. A diferencia de los tipos, los argumentos de
tipo a menudo se pueden deducir de los argumentos de una llamada al método y no es necesario
proporcionarlos explícitamente.
La signatura de un método debe ser única en la clase en la que se declara el método. La signatura de un
método se compone del nombre del método, el número de parámetros de tipo y el número, los modificadores y
los tipos de sus parámetros. La signatura de un método no incluye el tipo de valor devuelto.
Parámetros
Los parámetros se usan para pasar valores o referencias a variables a métodos. Los parámetros de un método
obtienen sus valores reales de los argumentos que se especifican cuando se invoca el método. Hay cuatro
tipos de parámetros: parámetros de valor, parámetros de referencia, parámetros de salida y matrices de
parámetros.
Un parámetro de valor se usa para el paso de parámetros de entrada. Un parámetro de valor corresponde a
una variable local que obtiene su valor inicial del argumento que se ha pasado para el parámetro. Las
modificaciones en un parámetro de valor no afectan el argumento que se pasa para el parámetro.
Los parámetros de valor pueden ser opcionales; se especifica un valor predeterminado para que se puedan
omitir los argumentos correspondientes.
Un parámetro de referencia se usa para el paso de parámetros de entrada y salida. El argumento pasado
para un parámetro de referencia debe ser una variable, y durante la ejecución del método, el parámetro de
referencia representa la misma ubicación de almacenamiento que la variable del argumento. Un parámetro de
referencia se declara con el modificador ref . En el ejemplo siguiente se muestra el uso de parámetros ref .
using System;
class Test
{
static void Swap(ref int x, ref int y) {
int temp = x;
x = y;
y = temp;
}
Un parámetro de salida se usa para pasar parámetros de salida. Un parámetro de salida es similar a un
parámetro de referencia, salvo que el valor inicial del argumento proporcionado por el llamador no es
importante. Un parámetro de salida se declara con el modificador out . En el ejemplo siguiente se muestra el
uso de parámetros out .
using System;
class Test
{
static void Divide(int x, int y, out int result, out int remainder) {
result = x / y;
remainder = x % y;
}
Una matriz de parámetros permite que se pasen a un método un número variable de argumentos. Una
matriz de parámetros se declara con el modificador params . Solo el último parámetro de un método puede ser
una matriz de parámetros y el tipo de una matriz de parámetros debe ser un tipo de matriz unidimensional. Los
métodos Write y WriteLine de la clase System.Console son buenos ejemplos de uso de la matriz de
parámetros. Se declaran de la manera siguiente.
Dentro de un método que usa una matriz de parámetros, la matriz de parámetros se comporta exactamente
igual que un parámetro normal de un tipo de matriz. Sin embargo, en una invocación de un método con una
matriz de parámetros, es posible pasar un único argumento del tipo de matriz de parámetros o cualquier
número de argumentos del tipo de elemento de la matriz de parámetros. En este caso, una instancia de matriz
se e inicializa automáticamente con los argumentos dados. Este ejemplo
using System;
class Squares
{
static void Main() {
int i = 0;
int j;
while (i < 10) {
j = i * i;
Console.WriteLine("{0} x {0} = {1}", i, j);
i = i + 1;
}
}
}
C# requiere que se asigne definitivamente una variable local antes de que se pueda obtener su valor. Por
ejemplo, si la declaración de i anterior no incluyera un valor inicial, el compilador notificaría un error con los
usos posteriores de i porque i no se asignaría definitivamente en esos puntos del programa.
Puede usar una instrucción return para devolver el control a su llamador. En un método que devuelve void ,
las instrucciones return no pueden especificar una expresión. En un método que devuelve void instrucciones
no, return debe incluir una expresión que calcule el valor devuelto.
Métodos estáticos y de instancia
Un método declarado con un modificador static es un método estático . Un método estático no opera en
una instancia específica y solo puede acceder directamente a miembros estáticos.
Un método declarado sin un modificador static es un método de instancia . Un método de instancia opera
en una instancia específica y puede acceder a miembros estáticos y de instancia. Se puede acceder
explícitamente a la instancia en la que se invoca un método de instancia como this . Es un error hacer
referencia a this en un método estático.
La siguiente clase Entity tiene miembros estáticos y de instancia.
class Entity
{
static int nextSerialNo;
int serialNo;
public Entity() {
serialNo = nextSerialNo++;
}
Cada instancia Entity contiene un número de serie (y probablemente alguna otra información que no se
muestra aquí). El constructor Entity (que es como un método de instancia) inicializa la nueva instancia con el
siguiente número de serie disponible. Dado que el constructor es un miembro de instancia, se le permite
acceder al campo de instancia serialNo y al campo estático nextSerialNo .
Los métodos estáticos GetNextSerialNo y SetNextSerialNo pueden acceder al campo estático nextSerialNo ,
pero sería un error para ellas acceder directamente al campo de instancia serialNo .
En el ejemplo siguiente se muestra el uso de la clase Entity .
using System;
class Test
{
static void Main() {
Entity.SetNextSerialNo(1000);
Entity e1 = new Entity();
Entity e2 = new Entity();
Console.WriteLine(e1.GetSerialNo()); // Outputs "1000"
Console.WriteLine(e2.GetSerialNo()); // Outputs "1001"
Console.WriteLine(Entity.GetNextSerialNo()); // Outputs "1002"
}
}
Tenga en cuenta que los métodos estáticos SetNextSerialNo y GetNextSerialNo se invocan en la clase, mientras
que el método de instancia GetSerialNo se invoca en instancias de la clase.
Métodos virtual, de reemplazo y abstracto
Cuando una declaración de método de instancia incluye un virtual modificador, se dice que el método es un
*método vir tual _. Cuando no virtual existe ningún modificador, se dice que el método es un método no
virtual* *.
Cuando se invoca un método virtual, el tipo de tiempo de ejecución * de la instancia para la que tiene lugar la
invocación determina la implementación de método real que se va a invocar. En una invocación de método no
virtual, el tipo de tiempo de compilación _ * de la instancia es el factor determinante.
Un método virtual puede ser reemplazado en una clase derivada. Cuando una declaración de método de
instancia incluye un override modificador, el método invalida un método virtual heredado con la misma firma.
Mientras que una declaración de método virtual introduce un método nuevo, una declaración de método de
reemplazo especializa un método virtual heredado existente proporcionando una nueva implementación de ese
método.
Un método abstracto es un método virtual sin implementación. Un método abstracto se declara con el
abstract modificador y solo se permite en una clase que también se declara abstract . Un método abstracto
debe reemplazarse en todas las clases derivadas no abstractas.
En el ejemplo siguiente se declara una clase abstracta, Expression , que representa un nodo de árbol de
expresión y tres clases derivadas, Constant , VariableReference y Operation , que implementan nodos de árbol
de expresión para constantes, referencias a variables y operaciones aritméticas. (Esto es similar a, pero no debe
confundirse con los tipos de árbol de expresión introducidos en los tipos de árbol de expresión).
using System;
using System.Collections;
Las cuatro clases anteriores se pueden usar para modelar expresiones aritméticas. Por ejemplo, usando
instancias de estas clases, la expresión x + 3 se puede representar de la manera siguiente.
Expression e = new Operation(
new VariableReference("x"),
'+',
new Constant(3));
El método Evaluate de una instancia Expression se invoca para evaluar la expresión determinada y generar un
valor double . El método toma como argumento un Hashtable que contiene nombres de variables (como claves
de las entradas) y valores (como valores de las entradas). El Evaluate método es un método abstracto virtual, lo
que significa que las clases derivadas no abstractas deben invalidarlo para proporcionar una implementación
real.
Una implementación de Constant de Evaluate simplemente devuelve la constante almacenada. Una
VariableReference implementación de busca el nombre de la variable en la tabla hash y devuelve el valor
resultante. Una implementación de Operation evalúa primero los operandos izquierdo y derecho (mediante la
invocación recursiva de sus métodos Evaluate ) y luego realiza la operación aritmética correspondiente.
El siguiente programa usa las clases Expression para evaluar la expresión x * (y + 2) para los distintos
valores de x y y .
using System;
using System.Collections;
class Test
{
static void Main() {
Expression e = new Operation(
new VariableReference("x"),
'*',
new Operation(
new VariableReference("y"),
'+',
new Constant(2)
)
);
Hashtable vars = new Hashtable();
vars["x"] = 3;
vars["y"] = 5;
Console.WriteLine(e.Evaluate(vars)); // Outputs "21"
vars["x"] = 1.5;
vars["y"] = 9;
Console.WriteLine(e.Evaluate(vars)); // Outputs "16.5"
}
}
Sobrecarga de métodos
El método *Overloading _ permite que varios métodos de la misma clase tengan el mismo nombre siempre y
cuando tengan firmas únicas. Al compilar una invocación de un método sobrecargado, el compilador usa _
Overload Resolution* para determinar el método específico que se va a invocar. La resolución de sobrecarga
busca el método que mejor coincida con los argumentos o informa de un error si no se puede encontrar
ninguna mejor coincidencia. En el ejemplo siguiente se muestra la resolución de sobrecarga en vigor. El
comentario para cada invocación del método Main muestra qué método se invoca realmente.
class Test
{
static void F() {
Console.WriteLine("F()");
}
Tal como se muestra en el ejemplo, un método determinado siempre se puede seleccionar mediante la
conversión explícita de los argumentos en los tipos de parámetros exactos o el suministro explícito de los
argumentos de tipo.
Otros miembros de función
Los miembros que contienen código ejecutable se conocen colectivamente como miembros de función de
una clase. En la sección anterior se han descrito métodos, que son el tipo principal de los miembros de función.
En esta sección se describen los demás tipos de miembros de función admitidos por C#: constructores,
propiedades, indexadores, eventos, operadores y destructores.
En el código siguiente se muestra una clase genérica denominada List<T> , que implementa una lista de
objetos que se va a ampliar. La clase contiene varios ejemplos de los tipos más comunes de miembros de
función.
// Fields...
T[] items;
int count;
// Constructors...
public List(int capacity = defaultCapacity) {
items = new T[capacity];
}
// Properties...
public int Count {
get { return count; }
}
public int Capacity {
get {
return items.Length;
}
set {
if (value < count) value = count;
if (value != items.Length) {
T[] newItems = new T[value];
Array.Copy(items, 0, newItems, 0, count);
items = newItems;
}
}
}
// Indexer...
public T this[int index] {
get {
return items[index];
}
set {
items[index] = value;
OnChanged();
}
}
// Methods...
public void Add(T item) {
if (count == Capacity) Capacity = count * 2;
items[count] = item;
count++;
OnChanged();
}
protected virtual void OnChanged() {
if (Changed != null) Changed(this, EventArgs.Empty);
}
public override bool Equals(object other) {
return Equals(this, other as List<T>);
}
static bool Equals(List<T> a, List<T> b) {
if (a == null) return b == null;
if (b == null || a.count != b.count) return false;
for (int i = 0; i < a.count; i++) {
if (!object.Equals(a.items[i], b.items[i])) {
return false;
}
}
return true;
}
// Event...
public event EventHandler Changed;
// Operators...
public static bool operator ==(List<T> a, List<T> b) {
return Equals(a, b);
}
public static bool operator !=(List<T> a, List<T> b) {
return !Equals(a, b);
}
}
Constructores
C# admite constructores de instancia y estáticos. Un *constructor de instancia _ es un miembro que
implementa las acciones necesarias para inicializar una instancia de una clase. Un constructor _ static* es un
miembro que implementa las acciones necesarias para inicializar una clase en sí misma cuando se carga por
primera vez.
Un constructor se declara como un método sin ningún tipo de valor devuelto y el mismo nombre que la clase
contenedora. Si una declaración de constructor incluye un modificador static , declara un constructor estático.
De lo contrario, declara un constructor de instancia.
Los constructores de instancias se pueden sobrecargar. Por ejemplo, la clase List<T> declara dos constructores
de instancia, una sin parámetros y otra que toma un parámetro int . Los constructores de instancia se invocan
mediante el operador new . Las instrucciones siguientes asignan dos List<string> instancias de mediante cada
uno de los constructores de la List clase.
A diferencia de otros miembros, los constructores de instancia no se heredan y una clase no tiene ningún
constructor de instancia que no sea el que se declara realmente en la clase. Si no se proporciona ningún
constructor de instancia para una clase, se proporciona automáticamente uno vacío sin ningún parámetro.
Propiedades
*Proper ties _ son una extensión natural de los campos. Ambos son miembros con nombre con tipos asociados
y la sintaxis para acceder a los campos y las propiedades es la misma. Sin embargo, a diferencia de los campos,
las propiedades no denotan ubicaciones de almacenamiento. En su lugar, las propiedades tienen _ descriptores
de acceso* que especifican las instrucciones que se ejecutarán cuando se lean o escriban sus valores.
Una propiedad se declara como un campo, salvo que la declaración finaliza con un get descriptor de acceso o
un set descriptor de acceso escrito entre los delimitadores { y en } lugar de terminar en un punto y coma.
Una propiedad que tiene tanto un descriptor de acceso get como un set descriptor de acceso es una
propiedad * de lectura y escritura , una propiedad que solo tiene un get descriptor de acceso es una
propiedad de solo lectura y una propiedad que solo tiene un set descriptor de acceso es una propiedad de
solo escritura* *.
Un get descriptor de acceso corresponde a un método sin parámetros con un valor devuelto del tipo de
propiedad. Excepto como destino de una asignación, cuando se hace referencia a una propiedad en una
expresión, el get descriptor de acceso de la propiedad se invoca para calcular el valor de la propiedad.
Un set descriptor de acceso corresponde a un método con un solo parámetro denominado value y ningún
tipo de valor devuelto. Cuando se hace referencia a una propiedad como el destino de una asignación o como el
operando de ++ o -- , el set descriptor de acceso se invoca con un argumento que proporciona el nuevo
valor.
La clase List<T> declara dos propiedades, Count y Capacity , que son de solo lectura y de lectura y escritura,
respectivamente. El siguiente es un ejemplo de uso de estas propiedades.
De forma similar a los campos y métodos, C# admite propiedades de instancia y propiedades estáticas. Las
propiedades estáticas se declaran con el static modificador y las propiedades de instancia se declaran sin ella.
Los descriptores de acceso de una propiedad pueden ser virtuales. Cuando una declaración de propiedad
incluye un modificador virtual , abstract o override , se aplica a los descriptores de acceso de la propiedad.
Indexadores
Un indexador es un miembro que permite indexar de la misma manera que una matriz. Un indexador se
declara como una propiedad, excepto por el hecho que el nombre del miembro es this , seguido por una lista
de parámetros que se escriben entre los delimitadores [ y ] . Los parámetros están disponibles en los
descriptores de acceso del indexador. De forma similar a las propiedades, los indexadores pueden ser lectura y
escritura, de solo lectura y de solo escritura, y los descriptores de acceso de un indexador pueden ser virtuales.
La clase List declara un único indexador de lectura y escritura que toma un parámetro int . El indexador
permite indexar instancias de List con valores int . Por ejemplo
Los indexadores se pueden sobrecargar, lo que significa que una clase puede declarar varios indexadores
siempre y cuando el número o los tipos de sus parámetros sean diferentes.
Eventos
Un evento es un miembro que permite que una clase u objeto proporcionen notificaciones. Un evento se
declara como un campo, excepto por el hecho de que la declaración incluye una palabra clave event , y el tipo
debe ser un tipo delegado.
Dentro de una clase que declara un miembro de evento, el evento se comporta como un campo de un tipo
delegado (siempre que el evento no sea abstracto y no declare descriptores de acceso). El campo almacena una
referencia a un delegado que representa los controladores de eventos que se han agregado al evento. Si no hay
ningún controlador de eventos presente, el campo es null .
La clase List<T> declara un único miembro de evento llamado Changed , lo que indica que se ha agregado un
nuevo elemento a la lista. El Changed evento lo desencadena el OnChanged método virtual, que primero
comprueba si el evento es null (lo que significa que no hay ningún controlador presente). La noción de
generar un evento es equivalente exactamente a invocar el delegado representado por el evento; por lo tanto,
no hay ninguna construcción especial de lenguaje para generar eventos.
Los clientes reaccionan a los eventos mediante controladores de eventos . Los controladores de eventos se
asocian mediante el operador += y se quitan con el operador -= . En el ejemplo siguiente se asocia un
controlador de eventos con el evento Changed de un objeto List<string> .
using System;
class Test
{
static int changeCount;
Para escenarios avanzados donde se desea controlar el almacenamiento subyacente de un evento, una
declaración de evento puede proporcionar explícitamente los descriptores de acceso add y remove , que son
similares en cierto modo al descriptor de acceso set de una propiedad.
Operadores
Un operador es un miembro que define el significado de aplicar un operador de expresión determinado a las
instancias de una clase. Se pueden definir tres tipos de operadores: operadores unarios, operadores binarios y
operadores de conversión. Todos los operadores se deben declarar como public y static .
La clase List<T> declara dos operadores, operator== y operator!= , y de este modo proporciona un nuevo
significado a expresiones que aplican esos operadores a instancias List . En concreto, los operadores definen la
igualdad de dos instancias List<T> como la comparación de cada uno de los objetos contenidos con sus
métodos Equals . En el ejemplo siguiente se usa el operador == para comparar dos instancias List<int> .
using System;
class Test
{
static void Main() {
List<int> a = new List<int>();
a.Add(1);
a.Add(2);
List<int> b = new List<int>();
b.Add(1);
b.Add(2);
Console.WriteLine(a == b); // Outputs "True"
b.Add(3);
Console.WriteLine(a == b); // Outputs "False"
}
}
El primer objeto Console.WriteLine genera True porque las dos listas contienen el mismo número de objetos
con los mismos valores en el mismo orden. Si List<T> no hubiera definido operator== , el primer objeto
Console.WriteLine habría generado False porque a y b hacen referencia a diferentes instancias de
List<int> .
Destructores
Un destructor es un miembro que implementa las acciones necesarias para destruir una instancia de una clase.
Los destructores no pueden tener parámetros, no pueden tener modificadores de accesibilidad y no se pueden
invocar explícitamente. El destructor de una instancia se invoca automáticamente durante la recolección de
elementos no utilizados.
El recolector de elementos no utilizados tiene una latitud ancha a la hora de decidir cuándo recopilar objetos y
ejecutar destructores. En concreto, el tiempo de las invocaciones de destructor no es determinista y los
destructores se pueden ejecutar en cualquier subproceso. Por estas y otras razones, las clases deben
implementar destructores solo cuando no sean factibles otras soluciones.
La instrucción using proporciona un mejor enfoque para la destrucción de objetos.
Estructuras
Al igual que las clases, los structs son estructuras de datos que pueden contener miembros de datos y
miembros de función, pero a diferencia de las clases, los structs son tipos de valor y no requieren asignación del
montón. Una variable de un tipo de struct almacena directamente los datos del struct, mientras que una variable
de un tipo de clase almacena una referencia a un objeto asignado dinámicamente. Los tipos struct no admiten la
herencia especificada por el usuario y todos los tipos de struct se heredan implícitamente del tipo object .
Los structs son particularmente útiles para estructuras de datos pequeñas que tengan semánticas de valor. Los
números complejos, los puntos de un sistema de coordenadas o los pares clave-valor de un diccionario son
buenos ejemplos de structs. El uso de un struct en lugar de una clase para estructuras de datos pequeñas puede
suponer una diferencia sustancial en el número de asignaciones de memoria que realiza una aplicación. Por
ejemplo, el siguiente programa crea e inicializa una matriz de 100 puntos. Si Point se implementa como una
clase, se crean instancias de 101 objetos distintos: uno para la matriz y uno por cada uno de los 100 elementos.
class Point
{
public int x, y;
class Test
{
static void Main() {
Point[] points = new Point[100];
for (int i = 0; i < 100; i++) points[i] = new Point(i, i);
}
}
struct Point
{
public int x, y;
Ahora, se crea la instancia de un solo objeto: la de la matriz, y las instancias de Point se asignan en línea dentro
de la matriz.
Los structs se invocan con el operador new , pero eso no implica que se asigne memoria. En lugar de asignar
dinámicamente un objeto y devolver una referencia a él, un constructor de structs simplemente devuelve el
valor del struct propiamente dicho (normalmente en una ubicación temporal en la pila) y este valor se copia
luego cuando es necesario.
Con las clases, es posible que dos variables hagan referencia al mismo objeto y, que por tanto, las operaciones
en una variable afecten al objeto al que hace referencia la otra variable. Con los struct, cada variable tiene su
propia copia de los datos y no es posible que las operaciones en una afecten a la otra. Por ejemplo, la salida
generada por el fragmento de código siguiente depende de si Point es una clase o un struct.
Si Point es una clase, el resultado es 20 porque a y b hacen referencia al mismo objeto. Si Point es un
struct, el resultado es 10 porque la asignación de a a b crea una copia del valor, y esta copia no se ve
afectada por la asignación subsiguiente a a.x .
En el ejemplo anterior se resaltan dos de las limitaciones de los structs. En primer lugar, copiar un struct entero
normalmente es menos eficaz que copiar una referencia a un objeto, por lo que el paso de parámetros de
asignación y valor puede ser más costoso con structs que con tipos de referencia. En segundo lugar, a excepción
de los parámetros ref y out , no es posible crear referencias a structs, que excluyen su uso en varias
situaciones.
Matrices
*Array _ es una estructura de datos que contiene un número de variables a las que se tiene acceso a través de
índices calculados. Las variables contenidas en una matriz, también denominadas elementos de la matriz, son
todas del mismo tipo y este tipo se denomina el tipo de elemento _ * de la matriz.
Los tipos de matriz son tipos de referencia, y la declaración de una variable de matriz simplemente establece un
espacio reservado para una referencia a una instancia de matriz. Las instancias de matriz reales se crean
dinámicamente en tiempo de ejecución mediante el new operador. La operación new especifica la longitud de
la nueva instancia de matriz, que luego se fija para la vigencia de la instancia. Los índices de los elementos de
una matriz van de 0 a Length - 1 . El operador new inicializa automáticamente los elementos de una matriz a
su valor predeterminado, que, por ejemplo, es cero para todos los tipos numéricos y null para todos los tipos
de referencias.
En el ejemplo siguiente se crea una matriz de elementos int , se inicializa la matriz y se imprime el contenido
de la matriz.
using System;
class Test
{
static void Main() {
int[] a = new int[10];
for (int i = 0; i < a.Length; i++) {
a[i] = i * i;
}
for (int i = 0; i < a.Length; i++) {
Console.WriteLine("a[{0}] = {1}", i, a[i]);
}
}
}
En este ejemplo se crea y funciona en una *matriz unidimensional _. C# también admite matrices
multidimensionales. El número de dimensiones de un tipo de matriz, también conocido como el _ rango* del
tipo de matriz, es uno más el número de comas escritas entre los corchetes del tipo de matriz. En el ejemplo
siguiente se asigna una matriz unidimensional, bidimensional y de tres dimensiones.
La primera línea crea una matriz con tres elementos, cada uno de tipo int[] y cada uno con un valor inicial de
null . Las líneas posteriores inicializan entonces los tres elementos con referencias a instancias de matriz
individuales de longitud variable.
El operador new permite especificar los valores iniciales de los elementos de matriz mediante un inicializador
de matriz , que es una lista de las expresiones escritas entre los delimitadores { y } . En el ejemplo siguiente
se asigna e inicializa un tipo int[] con tres elementos.
Tenga en cuenta que la longitud de la matriz se deduce del número de expresiones entre { y } . Las
declaraciones de variable local y campo se pueden acortar más para que así no sea necesario reformular el tipo
de matriz.
Interfaces
Una interfaz define un contrato que se puede implementar mediante clases y structs. Una interfaz puede
contener métodos, propiedades, eventos e indexadores. Una interfaz no proporciona implementaciones de los
miembros que define, simplemente especifica los miembros que se deben proporcionar mediante clases o
structs que implementan la interfaz.
Las interfaces pueden usar herencia múltiple . En el ejemplo siguiente, la interfaz IComboBox hereda de
ITextBox y IListBox .
interface IControl
{
void Paint();
}
Las clases y los structs pueden implementar varias interfaces. En el ejemplo siguiente, la clase EditBox
implementa IControl y IDataBound .
interface IDataBound
{
void Bind(Binder b);
}
Cuando una clase o un struct implementan una interfaz determinada, las instancias de esa clase o struct se
pueden convertir implícitamente a ese tipo de interfaz. Por ejemplo
En casos donde una instancia no se conoce estáticamente para implementar una interfaz determinada, se
pueden usar conversiones de tipo dinámico. Por ejemplo, las siguientes instrucciones usan conversiones de tipo
dinámico para obtener las implementaciones de un objeto y de la IControl IDataBound interfaz. Dado que el
tipo real del objeto es EditBox , las conversiones se realizan correctamente.
En la EditBox clase anterior, el Paint método de la IControl interfaz y el Bind método de la IDataBound
interfaz se implementan mediante public miembros. C# también admite implementaciones explícitas de
miembros de interfaz , con las que la clase o el struct pueden evitar la creación de miembros public . Una
implementación de miembro de interfaz explícito se escribe con el nombre de miembro de interfaz completo.
Por ejemplo, la clase EditBox podría implementar los métodos IControl.Paint y IDataBound.Bind mediante
implementaciones de miembros de interfaz explícitos del modo siguiente.
public class EditBox: IControl, IDataBound
{
void IControl.Paint() {...}
void IDataBound.Bind(Binder b) {...}
}
Solo se puede acceder a los miembros de interfaz explícitos mediante el tipo de interfaz. Por ejemplo, la
implementación de IControl.Paint proporcionada por la EditBox clase anterior solo se puede invocar
convirtiendo primero la EditBox referencia al IControl tipo de interfaz.
Enumeraciones
Un tipo de enumeración es un tipo de valor distinto con un conjunto de constantes con nombre. En el
ejemplo siguiente se declara y se utiliza un tipo de enumeración denominado Color con tres valores
constantes,, Red Green y Blue .
using System;
enum Color
{
Red,
Green,
Blue
}
class Test
{
static void PrintColor(Color color) {
switch (color) {
case Color.Red:
Console.WriteLine("Red");
break;
case Color.Green:
Console.WriteLine("Green");
break;
case Color.Blue:
Console.WriteLine("Blue");
break;
default:
Console.WriteLine("Unknown color");
break;
}
}
Cada tipo de enumeración tiene un tipo entero correspondiente denominado el tipo subyacente del tipo de
enumeración. Un tipo de enumeración que no declara explícitamente un tipo subyacente tiene un tipo
subyacente de int . El formato de almacenamiento y el intervalo de valores posibles de un tipo enum vienen
determinados por su tipo subyacente. El conjunto de valores que puede tomar un tipo de enumeración no está
limitado por sus miembros de enumeración. En concreto, cualquier valor del tipo subyacente de una
enumeración se puede convertir al tipo de enumeración y es un valor válido distinto de ese tipo de
enumeración.
En el ejemplo siguiente se declara un tipo de enumeración denominado Alignment con un tipo subyacente de
sbyte .
Como se muestra en el ejemplo anterior, una declaración de miembro de enumeración puede incluir una
expresión constante que especifique el valor del miembro. El valor constante para cada miembro de la
enumeración debe estar en el intervalo del tipo subyacente de la enumeración. Cuando una declaración de
miembro de enumeración no especifica explícitamente un valor, el miembro recibe el valor cero (si es el primer
miembro del tipo de enumeración) o el valor del miembro de enumeración anterior textual más uno.
Los valores de enumeración se pueden convertir en valores enteros y viceversa mediante conversiones de tipos.
Por ejemplo
El valor predeterminado de cualquier tipo de enumeración es el valor entero cero convertido al tipo de
enumeración. En los casos en los que las variables se inicializan automáticamente en un valor predeterminado,
este es el valor dado a las variables de los tipos de enumeración. Para que el valor predeterminado de un tipo de
enumeración sea fácilmente disponible, el literal se 0 convierte implícitamente a cualquier tipo de
enumeración. Por tanto, el siguiente código es válido.
Color c = 0;
Delegados
Un tipo de delegado representa las referencias a métodos con una lista de parámetros determinada y un tipo
de valor devuelto. Los delegados permiten tratar métodos como entidades que se puedan asignar a variables y
se puedan pasar como parámetros. Los delegados son similares al concepto de punteros de función en otros
lenguajes, pero a diferencia de los punteros de función, los delegados están orientados a objetos y presentan
seguridad de tipos.
En el siguiente ejemplo se declara y usa un tipo de delegado denominado Function .
using System;
class Multiplier
{
double factor;
class Test
{
static double Square(double x) {
return x * x;
}
Una instancia del tipo de delegado Function puede hacer referencia a cualquier método que tome un
argumento double y devuelva un valor double . El método Apply aplica un elemento Function determinado a
los elementos de double[] y devuelve double[] con los resultados. En el método Main , Apply se usa para
aplicar tres funciones diferentes a un valor double[] .
Un delegado puede hacer referencia a un método estático (como Square o Math.Sin en el ejemplo anterior) o
un método de instancia (como m.Multiply en el ejemplo anterior). Un delegado que hace referencia a un
método de instancia también hace referencia a un objeto determinado y, cuando se invoca el método de
instancia a través del delegado, ese objeto se convierte en this en la invocación.
Los delegados también pueden crearse mediante funciones anónimas, que son "métodos insertados" que se
crean sobre la marcha. Las funciones anónimas pueden ver las variables locales de los métodos adyacentes. Por
lo tanto, el ejemplo de multiplicador anterior se puede escribir más fácilmente sin usar una Multiplier clase:
Una propiedad interesante y útil de un delegado es que no sabe ni necesita conocer la clase del método al que
hace referencia; lo único que importa es que el método al que se hace referencia tenga los mismos parámetros
y el tipo de valor devuelto que el delegado.
Atributos
Los tipos, los miembros y otras entidades en un programa de C # admiten modificadores que controlan ciertos
aspectos de su comportamiento. Por ejemplo, la accesibilidad de un método se controla mediante los
modificadores public , protected , internal y private . C # generaliza esta funcionalidad de manera que los
tipos de información declarativa definidos por el usuario se puedan adjuntar a las entidades del programa y
recuperarse en tiempo de ejecución. Los programas especifican esta información declarativa adicional mediante
la definición y el uso de atributos .
En el ejemplo siguiente se declara un atributo HelpAttribute que se puede colocar en entidades de programa
para proporcionar vínculos a la documentación asociada.
using System;
Todas las clases de atributo derivan de la System.Attribute clase base proporcionada por el .NET Framework.
Los atributos se pueden aplicar proporcionando su nombre, junto con cualquier argumento, entre corchetes,
justo antes de la declaración asociada. Si el nombre de un atributo termina en Attribute , esa parte del nombre
se puede omitir cuando se hace referencia al atributo. Por ejemplo, el atributo HelpAttribute se puede usar de
la manera siguiente.
[Help("http://msdn.microsoft.com/.../MyClass.htm")]
public class Widget
{
[Help("http://msdn.microsoft.com/.../MyClass.htm", Topic = "Display")]
public void Display(string text) {}
}
En este ejemplo se adjunta un HelpAttribute a la Widget clase y otro HelpAttribute al Display método en la
clase. Los constructores públicos de una clase de atributos controlan la información que se debe proporcionar
cuando el atributo se adjunta a una entidad de programa. Se puede proporcionar información adicional
haciendo referencia a las propiedades públicas de lectura y escritura de la clase de atributos (como la referencia
a la propiedad Topic usada anteriormente).
En el ejemplo siguiente se muestra cómo se puede recuperar la información de atributos de una entidad de
programa determinada en tiempo de ejecución mediante la reflexión.
using System;
using System.Reflection;
class Test
{
static void ShowHelp(MemberInfo member) {
HelpAttribute a = Attribute.GetCustomAttribute(member,
typeof(HelpAttribute)) as HelpAttribute;
if (a == null) {
Console.WriteLine("No help for {0}", member);
}
else {
Console.WriteLine("Help for {0}:", member);
Console.WriteLine(" Url={0}, Topic={1}", a.Url, a.Topic);
}
}
Cuando se solicita un atributo determinado mediante reflexión, se invoca al constructor de la clase de atributos
con la información proporcionada en el origen del programa y se devuelve la instancia de atributo resultante. Si
se proporciona información adicional mediante propiedades, dichas propiedades se establecen en los valores
dados antes de devolver la instancia del atributo.
Estructura léxica
18/09/2021 • 63 minutes to read
Programas
Un programa de C# * _ consta de uno o varios archivos de código fuente, conocido formalmente como
unidades de compilación* (unidades de compilación). Un archivo de origen es una secuencia ordenada de
caracteres Unicode. Los archivos de origen suelen tener una correspondencia uno a uno con los archivos de un
sistema de archivos, pero esta correspondencia no es necesaria. Para obtener la máxima portabilidad, se
recomienda codificar los archivos de un sistema de archivos con la codificación UTF-8.
En términos conceptuales, un programa se compila mediante tres pasos:
1. Transformación, que convierte un archivo de un repertorio de caracteres determinado y un esquema de
codificación en una secuencia de caracteres Unicode.
2. Análisis léxico, que convierte una secuencia de caracteres de entrada Unicode en una secuencia de tokens.
3. Análisis sintáctico, que convierte el flujo de tokens en código ejecutable.
Gramáticas
Esta especificación presenta la sintaxis del lenguaje de programación C# con dos gramáticas. La gramática
léxica _ (gramática léxica) define cómo se combinan los caracteres Unicode para formar terminadores de línea,
espacios en blanco, comentarios, tokens y directivas de procesamiento previo. La _ gramática sintáctica*
(gramática sintáctica) define el modo en que los tokens resultantes de la gramática léxica se combinan para
formar programas de C#.
Notación gramatical
Las gramáticas léxicas y sintácticas se presentan en Backus-Naur formulario mediante la notación de la
herramienta de gramática ANTLR.
Gramática léxica
La gramática léxica de C# se presenta en el análisis léxico, los tokensy las directivas de procesamiento previo.
Los símbolos de terminal de la gramática léxica son los caracteres del juego de caracteres Unicode y la
gramática léxica especifica cómo se combinan los caracteres para formar tokens (tokens), espacios en blanco
(espacio en blanco), comentarios (comentarios) y directivas de preprocesamiento (directivas de procesamiento
previo).
Cada archivo de código fuente de un programa de C# debe ajustarse a la producción de entrada de la gramática
léxica (análisis léxico).
Gramática sintáctica
La gramática sintáctica de C# se presenta en los capítulos y los apéndices que siguen este capítulo. Los símbolos
de terminal de la gramática sintáctica son los tokens definidos por la gramática léxica y la gramática sintáctica
especifica cómo se combinan los tokens para formar programas de C#.
Cada archivo de código fuente de un programa de C# debe ajustarse a la compilation_unit producción de la
gramática sintáctica (unidades de compilación).
Análisis léxico
La producción de entrada define la estructura léxica de un archivo de código fuente de C#. Cada archivo de
código fuente de un programa de C# debe ajustarse a esta producción de gramática léxica.
input
: input_section?
;
input_section
: input_section_part+
;
input_section_part
: input_element* new_line
| pp_directive
;
input_element
: whitespace
| comment
| token
;
Cinco elementos básicos componen la estructura léxica de un archivo de código fuente de C#: terminadores de
línea (terminadores de línea), espacios en blanco (espacios en blanco), comentarios (comentarios), tokens
(tokens) y directivas de preprocesamiento (directivas de procesamiento previo). De estos elementos básicos,
solo los tokens son significativos en la gramática sintáctica de un programa de C# (gramática sintáctica).
El procesamiento léxico de un archivo de código fuente de C# consiste en reducir el archivo en una secuencia de
tokens que se convierte en la entrada del análisis sintáctico. Los terminadores de línea, los espacios en blanco y
los comentarios pueden servir para separar los tokens y las directivas de procesamiento previo pueden
provocar que se omitan las secciones del archivo de código fuente, pero, de lo contrario, estos elementos léxicos
no tienen ningún impacto en la estructura sintáctica de un programa de C#.
En el caso de los literales de cadena interpolados (literales de cadena interpolados), un único token lo genera
inicialmente el análisis léxico, pero se divide en varios elementos de entrada que se someten repetidamente al
análisis léxico hasta que todos los literales de cadena interpolados se han resuelto. Los tokens resultantes sirven
como entrada para el análisis sintáctico.
Cuando varias producciones de gramática léxica coinciden con una secuencia de caracteres de un archivo de
código fuente, el procesamiento léxico siempre forma el elemento léxico más largo posible. Por ejemplo, la
secuencia de caracteres // se procesa como el principio de un Comentario de una sola línea porque ese
elemento léxico es más largo que un / token único.
Terminadores de línea
Los terminadores de línea dividen los caracteres de un archivo de código fuente de C# en líneas.
new_line
: '<Carriage return character (U+000D)>'
| '<Line feed character (U+000A)>'
| '<Carriage return character (U+000D) followed by line feed character (U+000A)>'
| '<Next line character (U+0085)>'
| '<Line separator character (U+2028)>'
| '<Paragraph separator character (U+2029)>'
;
Por compatibilidad con las herramientas de edición de código fuente que agregan marcadores de fin de archivo
y para permitir que un archivo de código fuente se vea como una secuencia de líneas terminadas correctamente,
se aplican las transformaciones siguientes, en orden, a cada archivo de código fuente de un programa de C#:
Si el último carácter del archivo de código fuente es un carácter control-Z ( U+001A ), este carácter se elimina.
Un carácter de retorno de carro ( U+000D ) se agrega al final del archivo de código fuente si ese archivo de
código fuente no está vacío y si el último carácter del archivo de código fuente no es un retorno de carro ()
U+000D , un salto de línea ( U+000A ), un separador de líneas ( U+2028 ) o un separador de párrafo ( U+2029
).
Comentarios
Se admiten dos formatos de comentarios: de una sola línea y delimitados.\ Comentarios de una sola línea:
empiece con los caracteres // y amplíe hasta el final de la línea de código fuente. Los _comentarios
delimitados_ comienzan con los caracteres /_ y terminan con los caracteres */ . Los comentarios delimitados
pueden abarcar varias líneas.
comment
: single_line_comment
| delimited_comment
;
single_line_comment
: '//' input_character*
;
input_character
: '<Any Unicode character except a new_line_character>'
;
new_line_character
: '<Carriage return character (U+000D)>'
| '<Line feed character (U+000A)>'
| '<Next line character (U+0085)>'
| '<Line separator character (U+2028)>'
| '<Paragraph separator character (U+2029)>'
;
delimited_comment
: '/*' delimited_comment_section* asterisk+ '/'
;
delimited_comment_section
: '/'
| asterisk* not_slash_or_asterisk
;
asterisk
: '*'
;
not_slash_or_asterisk
: '<Any Unicode character except / or *>'
;
Los comentarios no se anidan. Las secuencias de caracteres /* y */ no tienen ningún significado especial
dentro de un // Comentario y las secuencias de caracteres // y /* no tienen ningún significado especial
dentro de un comentario delimitado.
Los comentarios no se procesan dentro de los literales de carácter y de cadena.
En el ejemplo
/* Hello, world program
This program writes "hello, world" to the console
*/
class Hello
{
static void Main() {
System.Console.WriteLine("hello, world");
}
}
whitespace
: '<Any character with Unicode class Zs>'
| '<Horizontal tab character (U+0009)>'
| '<Vertical tab character (U+000B)>'
| '<Form feed character (U+000C)>'
;
Tokens
Hay varios tipos de tokens: identificadores, palabras clave, literales, operadores y signos de puntuación. Los
espacios en blanco y los comentarios no son tokens, aunque actúan como separadores de tokens.
token
: identifier
| keyword
| integer_literal
| real_literal
| character_literal
| string_literal
| interpolated_string_literal
| operator_or_punctuator
;
unicode_escape_sequence
: '\\u' hex_digit hex_digit hex_digit hex_digit
| '\\U' hex_digit hex_digit hex_digit hex_digit hex_digit hex_digit hex_digit hex_digit
;
Una secuencia de escape Unicode representa el carácter Unicode que forma el número hexadecimal después de
los \u caracteres "" o "" \U . Dado que C# usa una codificación de 16 bits de puntos de código Unicode en
caracteres y valores de cadena, no se permite un carácter Unicode en el intervalo de U + 10000 a U + 10FFFF en
un literal de carácter y se representa mediante un par suplente Unicode en un literal de cadena. No se admiten
los caracteres Unicode con puntos de código anteriores a 0x10FFFF.
No se realizan varias traducciones. Por ejemplo, el literal de cadena " \u005Cu005C " es equivalente a " \u005C "
en lugar de " \ ". El valor Unicode \u005C es el carácter " \ ".
En el ejemplo
class Class1
{
static void Test(bool \u0066) {
char c = '\u0066';
if (\u0066)
System.Console.WriteLine(c.ToString());
}
}
muestra varios usos de \u0066 , que es la secuencia de escape para la letra " f ". El programa es equivalente a
class Class1
{
static void Test(bool f) {
char c = 'f';
if (f)
System.Console.WriteLine(c.ToString());
}
}
Identificadores
Las reglas para los identificadores que se proporcionan en esta sección corresponden exactamente a las
recomendadas por el Anexo 31 del estándar Unicode, con la excepción de que el carácter de subrayado se
permite como carácter inicial (como el tradicional en el lenguaje de programación C), las secuencias de escape
Unicode se permiten en los identificadores y el @ carácter "
identifier
: available_identifier
| '@' identifier_or_keyword
;
available_identifier
: '<An identifier_or_keyword that is not a keyword>'
;
identifier_or_keyword
: identifier_start_character identifier_part_character*
;
identifier_start_character
: letter_character
| '_'
;
identifier_part_character
: letter_character
| decimal_digit_character
| connecting_character
| combining_character
| formatting_character
;
letter_character
: '<A Unicode character of classes Lu, Ll, Lt, Lm, Lo, or Nl>'
| '<A unicode_escape_sequence representing a character of classes Lu, Ll, Lt, Lm, Lo, or Nl>'
;
combining_character
: '<A Unicode character of classes Mn or Mc>'
| '<A unicode_escape_sequence representing a character of classes Mn or Mc>'
;
decimal_digit_character
: '<A Unicode character of the class Nd>'
| '<A unicode_escape_sequence representing a character of the class Nd>'
;
connecting_character
: '<A Unicode character of the class Pc>'
| '<A unicode_escape_sequence representing a character of the class Pc>'
;
formatting_character
: '<A Unicode character of the class Cf>'
| '<A unicode_escape_sequence representing a character of the class Cf>'
;
Para obtener información sobre las clases de caracteres Unicode mencionadas anteriormente, vea el estándar
Unicode, versión 3,0, sección 4,5.
Entre los ejemplos de identificadores válidos se incluyen " identifier1 ", " _identifier2 "y" @if ".
Un identificador en un programa conforme debe estar en el formato canónico definido por la forma de
normalización Unicode C, tal y como se define en el Anexo 15 del estándar Unicode. El comportamiento cuando
se encuentra un identificador no en la forma de normalización C está definido por la implementación; sin
embargo, no es necesario un diagnóstico.
El prefijo " @ " permite el uso de palabras clave como identificadores, lo que resulta útil al interactuar con otros
lenguajes de programación. El carácter @ no es realmente parte del identificador, por lo que el identificador
podría verse en otros idiomas como un identificador normal, sin el prefijo. Un identificador con un @ prefijo se
denomina identificador textual . Se permite el uso del @ prefijo para los identificadores que no son palabras
clave, pero se desaconseja encarecidamente como una cuestión de estilo.
El ejemplo:
class @class
{
public static void @static(bool @bool) {
if (@bool)
System.Console.WriteLine("true");
else
System.Console.WriteLine("false");
}
}
class Class1
{
static void M() {
cl\u0061ss.st\u0061tic(true);
}
}
define una clase denominada " class " con un método estático denominado " static " que toma un
parámetro con el nombre " bool ". Tenga en cuenta que, puesto que no se permiten los escapes de Unicode en
palabras clave, el token " cl\u0061ss " es un identificador y es el mismo identificador que " @class ".
Dos identificadores se consideran iguales si son idénticos después de aplicar las transformaciones siguientes, en
orden:
El prefijo " @ ", si se utiliza, se quita.
Cada unicode_escape_sequence se transforma en el carácter Unicode correspondiente.
Se quitan los formatting_character s.
Los identificadores que contienen dos caracteres de subrayado consecutivos ( U+005F ) se reservan para su uso
por parte de la implementación. Por ejemplo, una implementación podría proporcionar palabras clave
extendidas que comienzan con dos guiones bajos.
Palabras clave
Una palabra clave es una secuencia similar a un identificador de caracteres que está reservada y no se puede
usar como identificador excepto cuando está precedida por el @ carácter.
keyword
: 'abstract' | 'as' | 'base' | 'bool' | 'break'
| 'byte' | 'case' | 'catch' | 'char' | 'checked'
| 'class' | 'const' | 'continue' | 'decimal' | 'default'
| 'delegate' | 'do' | 'double' | 'else' | 'enum'
| 'event' | 'explicit' | 'extern' | 'false' | 'finally'
| 'fixed' | 'float' | 'for' | 'foreach' | 'goto'
| 'if' | 'implicit' | 'in' | 'int' | 'interface'
| 'internal' | 'is' | 'lock' | 'long' | 'namespace'
| 'new' | 'null' | 'object' | 'operator' | 'out'
| 'override' | 'params' | 'private' | 'protected' | 'public'
| 'readonly' | 'ref' | 'return' | 'sbyte' | 'sealed'
| 'short' | 'sizeof' | 'stackalloc' | 'static' | 'string'
| 'struct' | 'switch' | 'this' | 'throw' | 'true'
| 'try' | 'typeof' | 'uint' | 'ulong' | 'unchecked'
| 'unsafe' | 'ushort' | 'using' | 'virtual' | 'void'
| 'volatile' | 'while'
;
En algunos lugares de la gramática, los identificadores específicos tienen un significado especial, pero no son
palabras clave. Dichos identificadores se denominan a veces "palabras clave contextuales". Por ejemplo, dentro
de una declaración de propiedad, los get identificadores "" y " set " tienen un significado especial
(descriptores de acceso). Un identificador distinto de get o set nunca se permite en estas ubicaciones, por lo
que este uso no entra en conflicto con el uso de estas palabras como identificadores. En otros casos, como con
el identificador " var " en las declaraciones de variables locales con tipo implícito (declaraciones devariables
locales), una palabra clave contextual puede entrar en conflicto con los nombres declarados. En tales casos, el
nombre declarado tiene prioridad sobre el uso del identificador como palabra clave contextual.
Literales
Un literal es una representación de código fuente de un valor.
literal
: boolean_literal
| integer_literal
| real_literal
| character_literal
| string_literal
| null_literal
;
booleanos, literales
Hay dos valores literales booleanos: true y false .
boolean_literal
: 'true'
| 'false'
;
decimal_integer_literal
: decimal_digit+ integer_type_suffix?
;
decimal_digit
: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
;
integer_type_suffix
: 'U' | 'u' | 'L' | 'l' | 'UL' | 'Ul' | 'uL' | 'ul' | 'LU' | 'Lu' | 'lU' | 'lu'
;
hexadecimal_integer_literal
: '0x' hex_digit+ integer_type_suffix?
| '0X' hex_digit+ integer_type_suffix?
;
hex_digit
: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
| 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'a' | 'b' | 'c' | 'd' | 'e' | 'f';
Si el valor representado por un literal entero está fuera del intervalo del ulong tipo, se produce un error en
tiempo de compilación.
Como cuestión de estilo, se sugiere que " L " se use en lugar de " l " cuando se escriben literales de tipo
long , ya que es fácil confundir la letra " l " con el dígito " 1 ".
Para permitir que los valores y más pequeños posibles int long se escriban como literales enteros decimales,
existen las dos reglas siguientes:
Cuando un decimal_integer_literal con el valor 2147483648 (2 ^ 31) y no hay ningún integer_type_suffix
aparece como el token inmediatamente después de un token de operador unario menos (operador unario
menos), el resultado es una constante de tipo int con el valor-2147483648 (-2 ^ 31). En todas las demás
situaciones, tal decimal_integer_literal es de tipo uint .
Cuando un decimal_integer_literal con el valor 9223372036854775808 (2 ^ 63) y no integer_type_suffix o el
integer_type_suffix L o l aparece como el token inmediatamente después de un token de operador unario
menos (operador unario menos), el resultado es una constante de tipo long con el valor-
9.223.372.036.854.775.808 (-2 ^ 63). En todas las demás situaciones, tal decimal_integer_literal es de tipo
ulong .
Literales reales
Los literales reales se utilizan para escribir valores de tipos float , double y decimal .
real_literal
: decimal_digit+ '.' decimal_digit+ exponent_part? real_type_suffix?
| '.' decimal_digit+ exponent_part? real_type_suffix?
| decimal_digit+ exponent_part real_type_suffix?
| decimal_digit+ real_type_suffix
;
exponent_part
: 'e' sign? decimal_digit+
| 'E' sign? decimal_digit+
;
sign
: '+'
| '-'
;
real_type_suffix
: 'F' | 'f' | 'D' | 'd' | 'M' | 'm'
;
Si no se especifica ningún real_type_suffix , el tipo del literal real es double . De lo contrario, el sufijo de tipo real
determina el tipo del literal real, como se indica a continuación:
Un literal real con sufijo F o f es de tipo float . Por ejemplo, los literales 1f , 1.5f , 1e10f y 123.456F
son de tipo float .
Un literal real con sufijo D o d es de tipo double . Por ejemplo, los literales 1d , 1.5d , 1e10d y
123.456D son de tipo double .
Un literal real con sufijo M o m es de tipo decimal . Por ejemplo, los literales 1m , 1.5m , 1e10m y
123.456M son de tipo decimal . Este literal se convierte en un decimal valor mediante el uso del valor
exacto, y, si es necesario, se redondea al valor representable más cercano mediante el redondeo bancario (el
tipo decimal). Cualquier escala aparente en el literal se conserva a menos que el valor se redondee o el valor
sea cero (en cuyo caso, el signo y la escala serán 0). Por lo tanto, el literal se 2.900m analizará para formar el
decimal con el signo 0 , el coeficiente 2900 y la escala 3 .
Nota: la notación gramatical ANTLR hace que la siguiente confusión. En ANTLR, cuando escribe, \' representa
una comilla simple ' . Y, cuando se escribe \\ , representa una sola barra diagonal inversa \ . Por lo tanto, la
primera regla para un literal de carácter significa que empieza con una comilla simple, un carácter y, a
continuación, una comilla simple. Y las once secuencias de escape simples posibles son \' , \" , \\ , \0 ,
\a , \b , \f , \n , \r , \t , \v .
character_literal
: '\'' character '\''
;
character
: single_character
| simple_escape_sequence
| hexadecimal_escape_sequence
| unicode_escape_sequence
;
single_character
: '<Any character except \' (U+0027), \\ (U+005C), and new_line_character>'
;
simple_escape_sequence
: '\\\'' | '\\"' | '\\\\' | '\\0' | '\\a' | '\\b' | '\\f' | '\\n' | '\\r' | '\\t' | '\\v'
;
hexadecimal_escape_sequence
: '\\x' hex_digit hex_digit? hex_digit? hex_digit?;
Un carácter que sigue a un carácter de barra diagonal inversa ( \ ) en un carácter debe ser uno de los
caracteres siguientes: ' , " , \ , 0 , a , b , f , n , r ,, t u , U ,, x v . De lo contrario, se produce un
error en tiempo de compilación.
Una secuencia de escape hexadecimal representa un único carácter Unicode, con el valor formado por el
número hexadecimal siguiente a " \x ".
Si el valor representado por un literal de carácter es mayor que U+FFFF , se produce un error en tiempo de
compilación.
Una secuencia de escape de caracteres Unicode (secuencias de escape de caracteres Unicode) en un literal de
carácter debe estar en el intervalo de U+0000 U+FFFF .
Una secuencia de escape simple representa una codificación de caracteres Unicode, tal y como se describe en la
tabla siguiente.
\0 Null 0x0000
\a Alerta 0x0007
\b Retroceso 0x0008
string_literal
: regular_string_literal
| verbatim_string_literal
;
regular_string_literal
: '"' regular_string_literal_character* '"'
;
regular_string_literal_character
: single_regular_string_literal_character
| simple_escape_sequence
| hexadecimal_escape_sequence
| unicode_escape_sequence
;
single_regular_string_literal_character
: '<Any character except " (U+0022), \\ (U+005C), and new_line_character>'
;
verbatim_string_literal
: '@"' verbatim_string_literal_character* '"'
;
verbatim_string_literal_character
: single_verbatim_string_literal_character
| quote_escape_sequence
;
single_verbatim_string_literal_character
: '<any character except ">'
;
quote_escape_sequence
: '""'
;
string i = "one\r\ntwo\r\nthree";
string j = @"one
two
three";
muestra varios literales de cadena. El último literal de cadena, j , es un literal de cadena textual que abarca
varias líneas. Los caracteres entre comillas, incluidos los espacios en blanco, como los caracteres de nueva línea,
se conservan literalmente.
Dado que una secuencia de escape hexadecimal puede tener un número variable de dígitos hexadecimales, el
literal de cadena "\x123" contiene un carácter único con el valor hexadecimal 123. Para crear una cadena que
contenga el carácter con el valor hexadecimal 12 seguido del carácter 3, puede "\x00123" escribir "\x12" + "3"
en su lugar o.
El tipo de una string_literal es string .
Cada literal de cadena no tiene necesariamente como resultado una nueva instancia de cadena. Cuando dos o
más literales de cadena que son equivalentes de acuerdo con el operador de igualdad de cadena (operadores de
igualdad de cadena) aparecen en el mismo programa, estos literales de cadena hacen referencia a la misma
instancia de cadena. Por ejemplo, la salida generada por
class Test
{
static void Main() {
object a = "hello";
object b = "hello";
System.Console.WriteLine(a == b);
}
}
se True debe a que los dos literales hacen referencia a la misma instancia de cadena.
Literales de cadena interpolados
Los literales de cadena interpolados son similares a los literales de cadena, pero contienen huecos delimitados
por { y } , donde se pueden producir expresiones. En tiempo de ejecución, las expresiones se evalúan con el
propósito de tener sus formularios de texto sustituidos en la cadena en el lugar donde se produce el agujero. La
sintaxis y la semántica de la interpolación de cadenas se describen en la sección (cadenas interpoladas).
Al igual que los literales de cadena, los literales de cadena interpolados pueden ser normales o literales. Los
literales de cadena normales interpolados están delimitados por $" y " , y los literales de cadena textual
interpolados están delimitados por $@" y " .
Al igual que otros literales, el análisis léxico de un literal de cadena interpolada produce inicialmente un token
único, según la gramática siguiente. Sin embargo, antes del análisis sintáctico, el token único de un literal de
cadena interpolada se divide en varios tokens para las partes de la cadena que los rodean, y los elementos de
entrada que se producen en los huecos se analizan léxicamente de nuevo. Esto puede, a su vez, generar más
literales de cadena interpolados que se van a procesar, pero, si léxicamente correcto, dará lugar a una secuencia
de tokens para que los procese el análisis sintáctico.
interpolated_string_literal
: '$' interpolated_regular_string_literal
| '$' interpolated_verbatim_string_literal
;
interpolated_regular_string_literal
: interpolated_regular_string_whole
| interpolated_regular_string_start interpolated_regular_string_literal_body
interpolated_regular_string_end
;
interpolated_regular_string_literal_body
: regular_balanced_text
| interpolated_regular_string_literal_body interpolated_regular_string_mid regular_balanced_text
;
interpolated_regular_string_whole
: '"' interpolated_regular_string_character* '"'
;
interpolated_regular_string_start
: '"' interpolated_regular_string_character* '{'
;
interpolated_regular_string_mid
: interpolation_format? '}' interpolated_regular_string_characters_after_brace? '{'
;
interpolated_regular_string_end
: interpolation_format? '}' interpolated_regular_string_characters_after_brace? '"'
;
interpolated_regular_string_characters_after_brace
: interpolated_regular_string_character_no_brace
| interpolated_regular_string_characters_after_brace interpolated_regular_string_character
;
interpolated_regular_string_character
: single_interpolated_regular_string_character
| simple_escape_sequence
| hexadecimal_escape_sequence
| unicode_escape_sequence
| open_brace_escape_sequence
| close_brace_escape_sequence
;
interpolated_regular_string_character_no_brace
: '<Any interpolated_regular_string_character except close_brace_escape_sequence and any
hexadecimal_escape_sequence or unicode_escape_sequence designating } (U+007D)>'
;
single_interpolated_regular_string_character
: '<Any character except \" (U+0022), \\ (U+005C), { (U+007B), } (U+007D), and new_line_character>'
;
open_brace_escape_sequence
: '{{'
;
close_brace_escape_sequence
close_brace_escape_sequence
: '}}'
;
regular_balanced_text
: regular_balanced_text_part+
;
regular_balanced_text_part
: single_regular_balanced_text_character
| delimited_comment
| '@' identifier_or_keyword
| string_literal
| interpolated_string_literal
| '(' regular_balanced_text ')'
| '[' regular_balanced_text ']'
| '{' regular_balanced_text '}'
;
single_regular_balanced_text_character
: '<Any character except / (U+002F), @ (U+0040), \" (U+0022), $ (U+0024), ( (U+0028), ) (U+0029), [
(U+005B), ] (U+005D), { (U+007B), } (U+007D) and new_line_character>'
| '</ (U+002F), if not directly followed by / (U+002F) or * (U+002A)>'
;
interpolation_format
: ':' interpolation_format_character+
;
interpolation_format_character
: '<Any character except \" (U+0022), : (U+003A), { (U+007B) and } (U+007D)>'
;
interpolated_verbatim_string_literal
: interpolated_verbatim_string_whole
| interpolated_verbatim_string_start interpolated_verbatim_string_literal_body
interpolated_verbatim_string_end
;
interpolated_verbatim_string_literal_body
: verbatim_balanced_text
| interpolated_verbatim_string_literal_body interpolated_verbatim_string_mid verbatim_balanced_text
;
interpolated_verbatim_string_whole
: '@"' interpolated_verbatim_string_character* '"'
;
interpolated_verbatim_string_start
: '@"' interpolated_verbatim_string_character* '{'
;
interpolated_verbatim_string_mid
: interpolation_format? '}' interpolated_verbatim_string_characters_after_brace? '{'
;
interpolated_verbatim_string_end
: interpolation_format? '}' interpolated_verbatim_string_characters_after_brace? '"'
;
interpolated_verbatim_string_characters_after_brace
: interpolated_verbatim_string_character_no_brace
| interpolated_verbatim_string_characters_after_brace interpolated_verbatim_string_character
;
interpolated_verbatim_string_character
: single_interpolated_verbatim_string_character
| quote_escape_sequence
| open_brace_escape_sequence
| close_brace_escape_sequence
;
interpolated_verbatim_string_character_no_brace
: '<Any interpolated_verbatim_string_character except close_brace_escape_sequence>'
;
single_interpolated_verbatim_string_character
: '<Any character except \" (U+0022), { (U+007B) and } (U+007D)>'
;
verbatim_balanced_text
: verbatim_balanced_text_part+
;
verbatim_balanced_text_part
: single_verbatim_balanced_text_character
| comment
| '@' identifier_or_keyword
| string_literal
| interpolated_string_literal
| '(' verbatim_balanced_text ')'
| '[' verbatim_balanced_text ']'
| '{' verbatim_balanced_text '}'
;
single_verbatim_balanced_text_character
: '<Any character except / (U+002F), @ (U+0040), \" (U+0022), $ (U+0024), ( (U+0028), ) (U+0029), [
(U+005B), ] (U+005D), { (U+007B) and } (U+007D)>'
| '</ (U+002F), if not directly followed by / (U+002F) or * (U+002A)>'
;
Un token de interpolated_string_literal se interpreta como varios tokens y otros elementos de entrada como se
indica a continuación, en el orden de aparición en el interpolated_string_literal:
Las apariciones de lo siguiente se interpretan como tokens individuales independientes: el $ signo inicial, la
interpolated_regular_string_whole, la interpolated_regular_string_start, el interpolated_regular_string_mid,
interpolated_regular_string_end, interpolated_verbatim_string_whole, interpolated_verbatim_string_start,
interpolated_verbatim_string_mid y interpolated_verbatim_string_end.
Las repeticiones de regular_balanced_text y verbatim_balanced_text entre ellas se reprocesan como un
input_section (análisis léxico) y se reinterpretan como la secuencia resultante de los elementos de entrada. A
su vez, pueden incluir tokens literales de cadena interpolados que se van a reinterpretar.
El análisis sintáctico volverá a combinar los tokens en un interpolated_string_expression (cadenas interpoladas).
Ejemplos TODO
El literal null
null_literal
: 'null'
;
El null_literal se puede convertir implícitamente a un tipo de referencia o a un tipo que acepta valores NULL.
Operadores y signos de puntuación
Hay varios tipos de operadores y signos de puntuación. Los operadores se usan en expresiones para describir
las operaciones con uno o varios operandos implicados. Por ejemplo, la expresión a + b usa el operador +
para agregar los dos operandos a y b . Los signos de puntuación se usan para agrupar y separar.
operator_or_punctuator
: '{' | '}' | '[' | ']' | '(' | ')' | '.' | ',' | ':' | ';'
| '+' | '-' | '*' | '/' | '%' | '&' | '|' | '^' | '!' | '~'
| '=' | '<' | '>' | '?' | '??' | '::' | '++' | '--' | '&&' | '||'
| '->' | '==' | '!=' | '<=' | '>=' | '+=' | '-=' | '*=' | '/=' | '%='
| '&=' | '|=' | '^=' | '<<' | '<<=' | '=>'
;
right_shift
: '>>'
;
right_shift_assignment
: '>>='
;
La barra vertical de las producciones RIGHT_SHIFT y right_shift_assignment se utiliza para indicar que, a
diferencia de otras producciones en la gramática sintáctica, no se permiten caracteres de ningún tipo (ni siquiera
espacios en blanco) entre los tokens. Estas producciones se tratan de forma especial para habilitar el control
correcto de type_parameter_list s (parámetros de tipo).
pp_directive
: pp_declaration
| pp_conditional
| pp_line
| pp_diagnostic
| pp_region
| pp_pragma
;
#define A
#undef B
class C
{
#if A
void F() {}
#else
void G() {}
#endif
#if B
void H() {}
#else
void I() {}
#endif
}
class C
{
void F() {}
void I() {}
}
Por lo tanto, mientras que los dos programas son bastante diferentes y sintácticamente, son idénticos.
Símbolos de compilación condicional
La funcionalidad de compilación condicional proporcionada por #if las #elif directivas,, #else y #endif se
controla a través de expresiones de procesamiento previo (expresiones de procesamiento previo) y símbolos de
compilación condicional.
conditional_symbol
: '<Any identifier_or_keyword except true or false>'
;
Un símbolo de compilación condicional tiene dos Estados posibles: *Defined _ o _ undefined *. Al principio del
procesamiento léxico de un archivo de código fuente, un símbolo de compilación condicional es indefinido, a
menos que se haya definido explícitamente mediante un mecanismo externo (como una opción del compilador
de línea de comandos). Cuando #define se procesa una directiva, el símbolo de compilación condicional
denominado en esa Directiva se define en ese archivo de código fuente. El símbolo permanece definido hasta
que #undef se procesa una directiva para el mismo símbolo, o hasta que se alcanza el final del archivo de
código fuente. Una implicación de esto es que #define #undef las directivas y de un archivo de código fuente
no tienen ningún efecto en otros archivos de código fuente del mismo programa.
Cuando se hace referencia en una expresión de procesamiento previo, un símbolo de compilación condicional
definido tiene el valor booleano true y un símbolo de compilación condicional sin definir tiene el valor
booleano false . No hay ningún requisito de que los símbolos de compilación condicional se declaren
explícitamente antes de que se haga referencia a ellos en expresiones de procesamiento previo. En su lugar, los
símbolos no declarados simplemente están sin definir y, por tanto, tienen el valor false .
El espacio de nombres de los símbolos de compilación condicional es distinto y separado de todas las demás
entidades con nombre en un programa de C#. Solo se puede hacer referencia a los símbolos de compilación
condicional en las #define #undef directivas y y en las expresiones de procesamiento previo.
Expresiones de procesamiento previo
Las expresiones de procesamiento previo pueden producirse en las #if #elif directivas y. Los operadores !
, == , != && y || se permiten en las expresiones de procesamiento previo y los paréntesis se pueden usar
para la agrupación.
pp_expression
: whitespace? pp_or_expression whitespace?
;
pp_or_expression
: pp_and_expression
| pp_or_expression whitespace? '||' whitespace? pp_and_expression
;
pp_and_expression
: pp_equality_expression
| pp_and_expression whitespace? '&&' whitespace? pp_equality_expression
;
pp_equality_expression
: pp_unary_expression
| pp_equality_expression whitespace? '==' whitespace? pp_unary_expression
| pp_equality_expression whitespace? '!=' whitespace? pp_unary_expression
;
pp_unary_expression
: pp_primary_expression
| '!' whitespace? pp_unary_expression
;
pp_primary_expression
: 'true'
| 'false'
| conditional_symbol
| '(' whitespace? pp_expression whitespace? ')'
;
Cuando se hace referencia en una expresión de procesamiento previo, un símbolo de compilación condicional
definido tiene el valor booleano true y un símbolo de compilación condicional sin definir tiene el valor
booleano false .
La evaluación de una expresión de procesamiento previo siempre produce un valor booleano. Las reglas de
evaluación de una expresión de procesamiento previo son las mismas que las de una expresión constante
(expresiones constantes), salvo que las únicas entidades definidas por el usuario a las que se puede hacer
referencia son símbolos de compilación condicionales.
Directivas de declaración
Las directivas de declaración se utilizan para definir o anular la definición de símbolos de compilación
condicional.
pp_declaration
: whitespace? '#' whitespace? 'define' whitespace conditional_symbol pp_new_line
| whitespace? '#' whitespace? 'undef' whitespace conditional_symbol pp_new_line
;
pp_new_line
: whitespace? single_line_comment? new_line
;
El procesamiento de una #define Directiva hace que se defina el símbolo de compilación condicional dado,
empezando por la línea de código fuente que sigue a la Directiva. Del mismo modo, el procesamiento de una
#undef Directiva hace que el símbolo de compilación condicional dado quede sin definir, empezando por la
línea de código fuente que sigue a la Directiva.
Las #define #undef directivas y de un archivo de código fuente deben aparecer antes del primer token
(tokens) en el archivo de código fuente; de lo contrario, se produce un error en tiempo de compilación. En
términos intuitivos, #define y #undef las directivas deben preceder a cualquier "código real" en el archivo de
código fuente.
El ejemplo:
#define Enterprise
namespace Megacorp.Data
{
#if Advanced
class PivotTable {...}
#endif
}
es válido porque las #define directivas preceden al primer token (la namespace palabra clave) en el archivo de
código fuente.
En el ejemplo siguiente se produce un error en tiempo de compilación porque un #define sigue código real:
#define A
namespace N
{
#define B
#if B
class Class1 {}
#endif
}
#define Puede definir un símbolo de compilación condicional que ya esté definido, sin que intervenga ningún
#undef símbolo. En el ejemplo siguiente se define un símbolo de compilación condicional A y, a continuación,
se define de nuevo.
#define A
#define A
Un #undef puede "anular la definición" de un símbolo de compilación condicional que no está definido. En el
ejemplo siguiente se define un símbolo de compilación condicional A y, a continuación, se anula su definición
dos veces; aunque el segundo #undef no tiene ningún efecto, sigue siendo válido.
#define A
#undef A
#undef A
pp_conditional
: pp_if_section pp_elif_section* pp_else_section? pp_endif
;
pp_if_section
: whitespace? '#' whitespace? 'if' whitespace pp_expression pp_new_line conditional_section?
;
pp_elif_section
: whitespace? '#' whitespace? 'elif' whitespace pp_expression pp_new_line conditional_section?
;
pp_else_section:
| whitespace? '#' whitespace? 'else' pp_new_line conditional_section?
;
pp_endif
: whitespace? '#' whitespace? 'endif' pp_new_line
;
conditional_section
: input_section
| skipped_section
;
skipped_section
: skipped_section_part+
;
skipped_section_part
: skipped_characters? new_line
| pp_directive
;
skipped_characters
: whitespace? not_number_sign input_character*
;
not_number_sign
: '<Any input_character except #>'
;
Como se indica en la sintaxis, las directivas de compilación condicional se deben escribir como conjuntos
compuestos de, en orden, una #if Directiva, cero o más #elif directivas, cero o una #else Directiva y una
#endif Directiva. Entre las directivas se encuentran las secciones condicionales del código fuente. Cada sección
se controla mediante la Directiva inmediatamente anterior. Una sección condicional puede contener directivas
de compilación condicional anidadas, siempre que estas directivas formen conjuntos completos.
Un pp_conditional selecciona como máximo uno de los conditional_section s incluidos para el procesamiento
léxico normal:
Los pp_expression s de las #if #elif directivas y se evalúan en orden hasta que se produce una true . Si
una expresión produce true , se selecciona la conditional_section de la directiva correspondiente.
Si todos los pp_expression producen false y, si una #else Directiva está presente, se selecciona el
conditional_section de la #else Directiva.
De lo contrario, no se selecciona ningún conditional_section .
La conditional_section seleccionada, si la hay, se procesa como una input_section normal: el código fuente
contenido en la sección debe adherirse a la gramática léxica; los tokens se generan a partir del código fuente de
la sección. y las directivas de procesamiento previo de la sección tienen los efectos prescritos.
El resto de conditional_section s, si los hay, se procesan como skipped_section s: excepto las directivas de
procesamiento previo, el código fuente de la sección no necesita adherirse a la gramática léxica; no se generan
tokens a partir del código fuente de la sección; y las directivas de procesamiento previo de la sección deben ser
léxicamente correctas, pero no se procesan de otra manera. Dentro de un conditional_section que se está
procesando como una skipped_section, cualquier conditional_section anidado (contenido en construcciones
anidadas #if ... #endif y #region ... #endregion ) también se procesa como skipped_section s.
En el ejemplo siguiente se muestra cómo se pueden anidar las directivas de compilación condicional:
class PurchaseTransaction
{
void Commit() {
#if Debug
CheckConsistency();
#if Trace
WriteToLog(this.ToString());
#endif
#endif
CommitHelper();
}
}
Excepto en el caso de las directivas de procesamiento previo, el código fuente omitido no está sujeto al análisis
léxico. Por ejemplo, lo siguiente es válido a pesar del comentario sin terminar en la #else sección:
class PurchaseTransaction
{
void Commit() {
#if Debug
CheckConsistency();
#else
/* Do something else
#endif
}
}
Sin embargo, tenga en cuenta que las directivas de procesamiento previo deben ser léxicamente correctas
incluso en secciones omitidas del código fuente.
Las directivas de procesamiento previo no se procesan cuando aparecen dentro de elementos de entrada de
varias líneas. Por ejemplo, el programa:
class Hello
{
static void Main() {
System.Console.WriteLine(@"hello,
#if Debug
world
#else
Nebraska
#endif
");
}
}
hello,
#if Debug
world
#else
Nebraska
#endif
En casos peculiares, el conjunto de directivas de procesamiento previo que se procesa puede depender de la
evaluación del pp_expression. El ejemplo:
#if X
/*
#else
/* */ class Q { }
#endif
siempre genera el mismo flujo de token ( class Q { } ), independientemente de si X está definido o no. Si
X se define, las únicas directivas procesadas son #if y #endif , debido al comentario de varias líneas. Si X
es undefined, tres directivas ( #if , #else , #endif ) forman parte del conjunto de directivas.
Directivas de diagnóstico
Las directivas de diagnóstico se utilizan para generar explícitamente mensajes de error y de advertencia que se
detectan de la misma manera que otros errores y advertencias en tiempo de compilación.
pp_diagnostic
: whitespace? '#' whitespace? 'error' pp_message
| whitespace? '#' whitespace? 'warning' pp_message
;
pp_message
: new_line
| whitespace input_character* new_line
;
El ejemplo:
#warning Code review needed before check-in
siempre genera una advertencia ("se requiere una revisión del código antes de la inserción en el repositorio") y
genera un error en tiempo de compilación ("una compilación no puede ser Debug and Retail") si se definen los
símbolos condicionales Debug y Retail . Tenga en cuenta que un pp_message puede contener texto arbitrario;
en concreto, no es necesario que los tokens sean correctos, tal y como se muestra en la palabra comilla simple
can't .
Directivas de región
Las directivas region se usan para marcar explícitamente las regiones del código fuente.
pp_region
: pp_start_region conditional_section? pp_end_region
;
pp_start_region
: whitespace? '#' whitespace? 'region' pp_message
;
pp_end_region
: whitespace? '#' whitespace? 'endregion' pp_message
;
No se adjunta ningún significado semántico a una región; las regiones están pensadas para que las use el
programador o las herramientas automatizadas para marcar una sección del código fuente. El mensaje
especificado en una #region Directiva o del #endregion mismo modo no tiene ningún significado semántico;
simplemente sirve para identificar la región. La coincidencia #region de las #endregion directivas y puede
tener distintos pp_message.
El procesamiento léxico de una región:
#region
...
#endregion
corresponde exactamente al procesamiento léxico de una directiva de compilación condicional del formulario:
#if true
...
#endif
Directivas de línea
Las directivas de línea se pueden usar para modificar los números de línea y los nombres de archivo de origen
que el compilador indica en la salida, como advertencias y errores, y que se usan en los atributos de
información de llamador (atributos de información de llamador).
Las directivas de línea se utilizan normalmente en herramientas de metaprogramaciones que generan código
fuente de C# a partir de otra entrada de texto.
pp_line
: whitespace? '#' whitespace? 'line' whitespace line_indicator pp_new_line
;
line_indicator
: decimal_digit+ whitespace file_name
| decimal_digit+
| 'default'
| 'hidden'
;
file_name
: '"' file_name_character+ '"'
;
file_name_character
: '<Any input_character except ">'
;
Cuando no hay #line directivas presentes, el compilador informa de los números de línea verdaderos y los
nombres de archivo de origen en su salida. Al procesar una #line Directiva que incluye un line_indicator que
no es default , el compilador trata la línea después de la Directiva con el número de línea especificado (y el
nombre de archivo, si se especifica).
Una #line default Directiva invierte el efecto de todas las directivas de #line anteriores. El compilador notifica
la información de línea verdadera para las líneas siguientes, exactamente como si no #line se hubieran
procesado directivas.
Una #line hidden Directiva no tiene ningún efecto en el archivo y los números de línea indicados en los
mensajes de error, pero afecta a la depuración de nivel de origen. Al depurar, todas las líneas entre una
#line hidden Directiva y la #line Directiva subsiguiente (que no es #line hidden ) no tienen información de
número de línea. Al recorrer el código en el depurador, estas líneas se omitirán por completo.
Tenga en cuenta que un file_name difiere de un literal de cadena normal en el que no se procesan los caracteres
de escape; el \ carácter "" simplemente designa un carácter de barra diagonal inversa normal dentro de un
file_name.
Directivas pragma
La #pragma Directiva de preprocesamiento se usa para especificar información contextual opcional para el
compilador. La información proporcionada en una #pragma directiva nunca cambiará la semántica del
programa.
pp_pragma
: whitespace? '#' whitespace? 'pragma' whitespace pragma_body pp_new_line
;
pragma_body
: pragma_warning_body
;
C# proporciona #pragma directivas para controlar las advertencias del compilador. Las versiones futuras del
lenguaje pueden incluir #pragma directivas adicionales. Para garantizar la interoperabilidad con otros
compiladores de C#, el compilador de Microsoft C# no emite errores de compilación para las directivas
desconocidas #pragma ; sin embargo, estas directivas generan advertencias.
ADVERTENCIA de pragma
La #pragma warning Directiva se usa para deshabilitar o restaurar todo o un conjunto determinado de mensajes
de advertencia durante la compilación del texto del programa subsiguiente.
pragma_warning_body
: 'warning' whitespace warning_action
| 'warning' whitespace warning_action whitespace warning_list
;
warning_action
: 'disable'
| 'restore'
;
warning_list
: decimal_digit+ (whitespace? ',' whitespace? decimal_digit+)*
;
Una #pragma warning Directiva que omite la lista de advertencias afecta a todas las advertencias. Una
#pragma warning Directiva que incluye una lista de advertencias afecta solo a las advertencias especificadas en
la lista.
Una #pragma warning disable Directiva deshabilita todos o el conjunto de advertencias especificado.
Una #pragma warning restore Directiva restaura todos o el conjunto de advertencias especificado en el estado
que estaba en vigor al principio de la unidad de compilación. Tenga en cuenta que si se ha deshabilitado una
advertencia determinada externamente, una #pragma warning restore (ya sea para toda o la advertencia
específica) no volverá a habilitar esa advertencia.
En el ejemplo siguiente se muestra el uso de #pragma warning para deshabilitar temporalmente la advertencia
que se indica cuando se hace referencia a los miembros obsoletos, mediante el número de advertencia del
compilador de Microsoft C#.
using System;
class Program
{
[Obsolete]
static void Foo() {}
Inicio de la aplicación
Un ensamblado que tiene un punto de entrada * _ se denomina aplicación. Cuando se ejecuta una aplicación,
se crea un nuevo _ _ *dominio de aplicación**. Puede haber varias instancias diferentes de una aplicación en el
mismo equipo al mismo tiempo, y cada una tiene su propio dominio de aplicación.
Un dominio de aplicación permite el aislamiento de aplicaciones actuando como un contenedor para el estado
de la aplicación. Un dominio de aplicación actúa como contenedor y límite para los tipos definidos en la
aplicación y las bibliotecas de clases que usa. Los tipos que se cargan en un dominio de aplicación son distintos
del mismo tipo cargado en otro dominio de aplicación y las instancias de objetos no se comparten directamente
entre los dominios de aplicación. Por ejemplo, cada dominio de aplicación tiene su propia copia de variables
estáticas para estos tipos, y un constructor estático para un tipo se ejecuta como máximo una vez por dominio
de aplicación. Las implementaciones son gratuitas para proporcionar directivas específicas de implementación o
mecanismos para la creación y destrucción de dominios de aplicación.
El inicio de la aplicación se produce cuando el entorno de ejecución llama a un método designado, al que se
hace referencia como punto de entrada de la aplicación. Este método de punto de entrada siempre se denomina
Main y puede tener una de las firmas siguientes:
Como se muestra, el punto de entrada puede devolver opcionalmente un int valor. Este valor devuelto se usa
en la finalización de la aplicación (finalizaciónde la aplicación).
El punto de entrada puede tener opcionalmente un parámetro formal. El parámetro puede tener cualquier
nombre, pero el tipo del parámetro debe ser string[] . Si el parámetro formal está presente, el entorno de
ejecución crea y pasa un string[] argumento que contiene los argumentos de la línea de comandos que se
especificaron al iniciarse la aplicación. El string[] argumento nunca es null, pero puede tener una longitud de
cero si no se especificó ningún argumento de línea de comandos.
Dado que C# admite la sobrecarga de métodos, una clase o un struct pueden contener varias definiciones de
algún método, siempre que cada una tenga una firma diferente. Sin embargo, dentro de un único programa,
ninguna clase o struct puede contener más de un método denominado Main cuya definición lo califique como
punto de entrada de la aplicación. No obstante, se permiten otras versiones sobrecargadas de, Main siempre
que tengan más de un parámetro, o su único parámetro sea distinto del tipo string[] .
Una aplicación puede estar formada por varias clases o Structs. Es posible que más de una de estas clases o
Structs contengan un método denominado Main cuya definición sea apta para su uso como punto de entrada
de la aplicación. En estos casos, se debe usar un mecanismo externo (como una opción del compilador de línea
de comandos) para seleccionar uno de estos Main métodos como punto de entrada.
En C#, cada método se debe definir como miembro de una clase o struct. Normalmente, la accesibilidad
declarada (accesibilidad declarada) de un método viene determinada por los modificadores de acceso
(modificadores de acceso) especificados en su declaración y, de igual forma, la accesibilidad declarada de un tipo
viene determinada por los modificadores de acceso especificados en su declaración. Para que se pueda llamar a
un método dado de un tipo determinado, tanto el tipo como el miembro deben ser accesibles. Sin embargo, el
punto de entrada de la aplicación es un caso especial. En concreto, el entorno de ejecución puede tener acceso al
punto de entrada de la aplicación, independientemente de su accesibilidad declarada y sin tener en
consideración la accesibilidad declarada de sus declaraciones de tipos envolventes.
Es posible que el método de punto de entrada de la aplicación no esté en una declaración de clase genérica.
En todos los demás aspectos, los métodos de punto de entrada se comportan como los que no son puntos de
entrada.
Finalización de aplicaciones
La finalización de la aplicación devuelve el control al entorno de ejecución.
Si el tipo de valor devuelto del método de punto de entrada * de la aplicación es int , el valor devuelto actúa
como el código de estado de terminación _ * de la aplicación * *. El propósito de este código es permitir la
comunicación de éxito o error en el entorno de ejecución.
Si el tipo de valor devuelto del método de punto de entrada es void , al alcanzar la llave de } cierre () que
finaliza ese método, o la ejecución de una return instrucción que no tiene ninguna expresión, da como
resultado un código de estado de finalización de 0 .
Antes de la finalización de una aplicación, se llama a los destructores para todos sus objetos que todavía no se
han recolectado como elemento no utilizado, a menos que se haya suprimido dicha limpieza (por ejemplo,
mediante una llamada al método de biblioteca GC.SuppressFinalize ).
Declaraciones
Las declaraciones de un programa de C# definen los elementos constituyentes del programa. Los programas de
C# se organizan mediante espacios de nombres (espaciosde nombres), que pueden contener declaraciones de
tipos y declaraciones de espacio de nombres anidadas. Las declaraciones de tipos (declaraciones de tipo) se
utilizan para definir clases (clases), Structs (Structs), interfaces (interfaces), enumeraciones (enumeraciones) y
delegados (delegados). Los tipos de miembros permitidos en una declaración de tipo dependen del formulario
de la declaración de tipos. Por ejemplo, las declaraciones de clase pueden contener declaraciones de constantes
(constantes). campos (campos), métodos (métodos), propiedades (propiedades), eventos (eventos), indexadores
(indizadores), operadores (operadores), constructores de instancias (constructores de instancias), constructores
estáticos (constructores estáticos), destructores (destructores) y tipos anidados (tipos anidados).
Una declaración define un nombre en el espacio de declaración al que pertenece la declaración. A excepción
de los miembros sobrecargados (firmas y sobrecarga), es un error en tiempo de compilación tener dos o más
declaraciones que introducen miembros con el mismo nombre en un espacio de declaración. Nunca es posible
que un espacio de declaración contenga distintos tipos de miembros con el mismo nombre. Por ejemplo, un
espacio de declaración nunca puede contener un campo y un método con el mismo nombre.
Hay varios tipos diferentes de espacios de declaración, como se describe a continuación.
En todos los archivos de código fuente de un programa, namespace_member_declaration s sin
namespace_declaration de inclusión son miembros de un único espacio de declaración combinado
denominado espacio de declaración global .
Dentro de todos los archivos de código fuente de un programa, namespace_member_declaration s dentro de
namespace_declaration s que tienen el mismo nombre de espacio de nombres completo son miembros de
un solo espacio de declaración combinado.
Cada declaración de clase, struct o interfaz crea un nuevo espacio de declaración. Los nombres se introducen
en este espacio de declaración a través de class_member_declaration s, struct_member_declaration s,
interface_member_declaration s o type_parameter s. A excepción de las declaraciones de constructor de
instancia sobrecargadas y las declaraciones de constructor estático, una clase o struct no puede contener una
declaración de miembro con el mismo nombre que la clase o la estructura. Una clase, estructura o interfaz
permite la declaración de métodos sobrecargados e indexadores. Además, una clase o struct permite la
declaración de constructores de instancia sobrecargados y operadores. Por ejemplo, una clase, un struct o
una interfaz pueden contener varias declaraciones de método con el mismo nombre, siempre que estas
declaraciones de método difieran en su firma (firmas y sobrecarga). Tenga en cuenta que las clases base no
contribuyen al espacio de declaración de una clase, y las interfaces base no contribuyen al espacio de
declaración de una interfaz. Por lo tanto, una clase derivada o una interfaz pueden declarar un miembro con
el mismo nombre que un miembro heredado. Este miembro se dice que oculta el miembro heredado.
Cada declaración de delegado crea un nuevo espacio de declaración. Los nombres se introducen en este
espacio de declaración a través de parámetros formales (fixed_parameter s y parameter_array s) y
type_parameter s.
Cada declaración de enumeración crea un nuevo espacio de declaración. Los nombres se introducen en este
espacio de declaración a través de enum_member_declarations.
Cada declaración de método, declaración de indexador, declaración de operador, declaración de constructor
de instancia y función anónima crea un nuevo espacio de declaración denominado *espacio de
declaración de variable local . Los nombres se introducen en este espacio de declaración a través de los
parámetros formales (_fixed_parameter * s y parameter_array s) y type_parameter s. El cuerpo del miembro
de función o de la función anónima, si existe, se considera anidado dentro del espacio de declaración de la
variable local. Es un error que un espacio de declaración de variable local y un espacio de declaración de
variable local anidada contengan elementos con el mismo nombre. Por lo tanto, dentro de un espacio de
declaración anidado no es posible declarar una variable o constante local con el mismo nombre que una
variable o constante local en un espacio de declaración de inclusión. Es posible que dos espacios de
declaración contengan elementos con el mismo nombre siempre y cuando ningún espacio de declaración
contenga el otro.
Cada bloque o switch_block , así como una instrucción for, foreach y using , crea un espacio de declaración
de variable local para las variables locales y las constantes locales. Los nombres se introducen en este
espacio de declaración a través de local_variable_declaration s y local_constant_declaration s. Tenga en
cuenta que los bloques que se producen como o dentro del cuerpo de un miembro de función o de una
función anónima están anidados dentro del espacio de declaración de variable local declarado por esas
funciones para sus parámetros. Por lo tanto, es un error tener, por ejemplo, un método con una variable local
y un parámetro con el mismo nombre.
Cada bloque o switch_block crea un espacio de declaración independiente para las etiquetas. Los nombres se
introducen en este espacio de declaración a través de labeled_statement s y se hace referencia a los nombres
a través de goto_statement s. El espacio de declaración de etiqueta de un bloque incluye cualquier
bloque anidado. Por lo tanto, dentro de un bloque anidado no es posible declarar una etiqueta con el mismo
nombre que una etiqueta en un bloque de inclusión.
El orden textual en el que se declaran los nombres no suele ser significativo. En concreto, el orden textual no es
significativo para la declaración y el uso de espacios de nombres, constantes, métodos, propiedades, eventos,
indizadores, operadores, constructores de instancias, destructores, constructores estáticos y tipos. El orden de
declaración es significativo de las siguientes maneras:
El orden de declaración de las declaraciones de campo y las declaraciones de variables locales determina el
orden en que se ejecutan sus inicializadores (si existen).
Las variables locales deben definirse antes de que se usen (ámbitos).
El orden de declaración para las declaraciones de miembros de enumeración (miembros de enumeración) es
importante cuando se omiten los valores de constant_expression .
El espacio de declaración de un espacio de nombres es "Open Terminated" y dos declaraciones de espacios de
nombres con el mismo nombre completo contribuyen al mismo espacio de declaración. Por ejemplo
namespace Megacorp.Data
{
class Customer
{
...
}
}
namespace Megacorp.Data
{
class Order
{
...
}
}
Las dos declaraciones de espacio de nombres anteriores contribuyen al mismo espacio de declaración; en este
caso, se declaran dos clases con los nombres completos Megacorp.Data.Customer y Megacorp.Data.Order . Dado
que las dos declaraciones contribuyen al mismo espacio de declaración, habría producido un error en tiempo de
compilación si cada una de ellas contenía una declaración de una clase con el mismo nombre.
Tal y como se especificó anteriormente, el espacio de declaración de un bloque incluye cualquier bloque
anidado. Por lo tanto, en el ejemplo siguiente, los F G métodos y generan un error en tiempo de compilación
porque el nombre i se declara en el bloque exterior y no se puede volver a declarar en el bloque interno. Sin
embargo, H los I métodos y son válidos, ya que los dos i se declaran en bloques independientes no
anidados.
class A
{
void F() {
int i = 0;
if (true) {
int i = 1;
}
}
void G() {
if (true) {
int i = 0;
}
int i = 1;
}
void H() {
if (true) {
int i = 0;
}
if (true) {
int i = 1;
}
}
void I() {
for (int i = 0; i < 10; i++)
H();
for (int i = 0; i < 10; i++)
H();
}
}
Miembros
Los espacios de nombres y los tipos tienen miembros. Los miembros de una entidad están disponibles con
carácter general a través del uso de un nombre completo que empieza por una referencia a la entidad, seguido
de un . token "", seguido del nombre del miembro.
Los miembros de un tipo se declaran en la declaración de tipos o se heredan de la clase base del tipo. Cuando
un tipo hereda de una clase base, todos los miembros de la clase base, excepto los constructores de instancia,
destructores y constructores estáticos, se convierten en miembros del tipo derivado. La accesibilidad declarada
de un miembro de clase base no controla si el miembro se hereda (la herencia se extiende a cualquier miembro
que no sea un constructor de instancia, un constructor estático o un destructor). Sin embargo, es posible que no
se pueda obtener acceso a un miembro heredado en un tipo derivado, ya sea debido a su accesibilidad
declarada (accesibilidad declarada) o porque está oculta por una declaración en el propio tipo (ocultando a
travésde la herencia).
Miembros del espacio de nombres
Los espacios de nombres y los tipos que no tienen ningún espacio de nombres envolvente son miembros del
espacio de nombres global . Se corresponde directamente con los nombres declarados en el espacio de
declaración global.
Los espacios de nombres y los tipos declarados dentro de un espacio de nombres son miembros de ese espacio
de nombres. Esto corresponde directamente a los nombres declarados en el espacio de declaración del espacio
de nombres.
Los espacios de nombres no tienen restricciones de acceso. No es posible declarar espacios de nombres
privados, protegidos o internos, y los nombres de los espacios de nombres siempre son accesibles
públicamente.
Miembros de estructuras
Los miembros de un struct son los miembros declarados en la estructura y los miembros heredados de la clase
base directa de la estructura System.ValueType y la clase base indirecta object .
Los miembros de un tipo simple se corresponden directamente con los miembros del tipo de struct con alias del
tipo simple:
Los miembros de sbyte son los miembros de la System.SByte estructura.
Los miembros de byte son los miembros de la System.Byte estructura.
Los miembros de short son los miembros de la System.Int16 estructura.
Los miembros de ushort son los miembros de la System.UInt16 estructura.
Los miembros de int son los miembros de la System.Int32 estructura.
Los miembros de uint son los miembros de la System.UInt32 estructura.
Los miembros de long son los miembros de la System.Int64 estructura.
Los miembros de ulong son los miembros de la System.UInt64 estructura.
Los miembros de char son los miembros de la System.Char estructura.
Los miembros de float son los miembros de la System.Single estructura.
Los miembros de double son los miembros de la System.Double estructura.
Los miembros de decimal son los miembros de la System.Decimal estructura.
Los miembros de bool son los miembros de la System.Boolean estructura.
Miembros de enumeración
Los miembros de una enumeración son las constantes declaradas en la enumeración y los miembros heredados
de la clase base directa de la enumeración System.Enum y las clases base indirectas System.ValueType y object .
Miembros de clase
Los miembros de una clase son los miembros declarados en la clase y los miembros heredados de la clase base
(excepto para la clase object que no tiene clase base). Los miembros heredados de la clase base incluyen las
constantes, campos, métodos, propiedades, eventos, indizadores, operadores y tipos de la clase base, pero no
los constructores de instancias, destructores y constructores estáticos de la clase base. Los miembros de clase
base se heredan sin tener en cuenta su accesibilidad.
Una declaración de clase puede contener declaraciones de constantes, campos, métodos, propiedades, eventos,
indizadores, operadores, constructores de instancias, destructores, constructores estáticos y tipos.
Los miembros de object y string corresponden directamente a los miembros de los tipos de clase a los que
se les ha contorno:
Los miembros de object son los miembros de la System.Object clase.
Los miembros de string son los miembros de la System.String clase.
Miembros de interfaz
Los miembros de una interfaz son los miembros declarados en la interfaz y en todas las interfaces base de la
interfaz. Los miembros de la clase object no son, estrictamente hablando, miembros de cualquier interfaz
(miembros de lainterfaz). Sin embargo, los miembros de la clase object están disponibles a través de la
búsqueda de miembros en cualquier tipo de interfaz (búsqueda de miembros).
Miembros de la matriz
Los miembros de una matriz son los miembros heredados de la clase System.Array .
Miembros de delegado
Los miembros de un delegado son los miembros heredados de la clase System.Delegate .
Acceso a miembros
Las declaraciones de miembros permiten el control sobre el acceso a miembros. La accesibilidad de un miembro
se establece mediante la accesibilidad declarada (accesibilidad declarada) del miembro combinado con la
accesibilidad del tipo contenedor inmediato, si existe.
Cuando se permite el acceso a un miembro determinado, se dice que el miembro es *accesible _. Por el
contrario, cuando no se permite el acceso a un miembro determinado, se dice que el miembro es _ inaccesible *.
Se permite el acceso a un miembro cuando la ubicación textual en la que tiene lugar el acceso se incluye en el
dominio de accesibilidad (dominios de accesibilidad) del miembro.
Accesibilidad declarada
La accesibilidad declarada de un miembro puede ser una de las siguientes:
Público, que se selecciona mediante la inclusión public de un modificador en la declaración de miembro. El
significado intuitivo de public es "acceso no limitado".
Protected, que se selecciona mediante protected la inclusión de un modificador en la declaración de
miembro. El significado intuitivo de protected es "acceso limitado a la clase contenedora o a los tipos
derivados de la clase contenedora".
Internal, que se selecciona mediante la inclusión internal de un modificador en la declaración de miembro.
El significado intuitivo de internal es "acceso limitado a este programa".
Protected internal (es decir, Protected o Internal), que se selecciona mediante la inclusión de protected y un
internal modificador en la declaración de miembro. El significado intuitivo de protected internal es
"acceso limitado a este programa o tipos derivados de la clase contenedora".
Private, que se selecciona incluyendo un private modificador en la declaración de miembro. El significado
intuitivo de private es "acceso limitado al tipo contenedor".
Dependiendo del contexto en el que tenga lugar una declaración de miembro, solo se permiten determinados
tipos de accesibilidad declarada. Además, cuando una declaración de miembro no incluye modificadores de
acceso, el contexto en el que se produce la declaración determina la accesibilidad declarada predeterminada.
Los espacios de nombres han declarado implícitamente la public accesibilidad. No se permiten
modificadores de acceso en las declaraciones de espacio de nombres.
Los tipos declarados en unidades de compilación o espacios de nombres pueden tener public o internal
declarar accesibilidad y de forma predeterminada como internal accesibilidad declarada.
Los miembros de clase pueden tener cualquiera de los cinco tipos de accesibilidad declarada y tienen como
valor predeterminado la private accesibilidad declarada. (Tenga en cuenta que un tipo declarado como
miembro de una clase puede tener cualquiera de los cinco tipos de accesibilidad declarada, mientras que un
tipo declarado como miembro de un espacio de nombres solo puede tener public o internal declarar
accesibilidad).
Los miembros de struct pueden tener public , internal o la private accesibilidad declarada y tienen
como valor predeterminado la private accesibilidad declarada porque los Structs están sellados
implícitamente. Los miembros de estructura introducidos en un struct (es decir, no heredados por ese struct)
no pueden tener protected o protected internal declarar accesibilidad. (Tenga en cuenta que un tipo
declarado como miembro de un struct puede tener public , internal o la private accesibilidad declarada,
mientras que un tipo declarado como miembro de un espacio de nombres solo puede tener public o
internal declarar accesibilidad).
Los miembros de interfaz han declarado de forma implícita la public accesibilidad. No se permiten
modificadores de acceso en las declaraciones de miembros de interfaz.
Los miembros de enumeración han declarado implícitamente la public accesibilidad. No se permiten
modificadores de acceso en las declaraciones de miembros de enumeración.
Dominios de accesibilidad
El dominio de accesibilidad * de un miembro se compone de las secciones (posiblemente disjuntos) del texto
del programa en el que se permite el acceso al miembro. A efectos de definir el dominio de accesibilidad de un
miembro, se dice que un miembro es de nivel superior si no se declara dentro de un tipo, y se dice que un
miembro está anidado si se declara dentro de otro tipo. Además, el texto del programa de un programa se
define como todo el texto del programa contenido en todos los archivos de código fuente del programa, y el
texto del programa de un tipo se define como todo el texto del programa incluido en el _type_declaration * s de
ese tipo (incluidos, posiblemente, los tipos anidados dentro del tipo).
El dominio de accesibilidad de un tipo predefinido (como object , int o double ) es ilimitado.
El dominio de accesibilidad de un tipo sin enlazar de nivel superior T (tipos enlazados y sin enlazar) que se
declara en un programa P se define de la manera siguiente:
Si la accesibilidad declarada de T es public , el dominio de accesibilidad de T es el texto de programa de
P y cualquier programa al que haga referencia P .
Si la accesibilidad declarada de T es internal , el dominio de accesibilidad de T es el texto de programa de
P .
A partir de estas definiciones, sigue que el dominio de accesibilidad de un tipo sin enlazar de nivel superior
siempre es al menos el texto del programa del programa en el que se declara ese tipo.
El dominio de accesibilidad de un tipo construido T<A1, ..., An> es la intersección del dominio de accesibilidad
del tipo genérico sin enlazar T y los dominios de accesibilidad de los argumentos de tipo A1, ..., An .
El dominio de accesibilidad de un miembro anidado M declarado en un tipo T dentro de un programa P se
define de la manera siguiente (teniendo en cuenta que M , por lo tanto, puede ser un tipo):
Si la accesibilidad declarada de M es public , el dominio de accesibilidad de M es el dominio de
accesibilidad de T .
Si la accesibilidad declarada de M es protected internal , permita que D sea la Unión del texto del
programa de P y el texto del programa de cualquier tipo derivado de T , que se declara fuera de P . El
dominio de accesibilidad de M es la intersección del dominio de accesibilidad de T con D .
Si la accesibilidad declarada de M es protected , permita que D sea la Unión del texto del programa de T
y el texto del programa de cualquier tipo derivado de T . El dominio de accesibilidad de M es la intersección
del dominio de accesibilidad de T con D .
Si la accesibilidad declarada de M es internal , el dominio de accesibilidad de M es la intersección del
dominio de accesibilidad de T con el texto de programa de P .
Si la accesibilidad declarada de M es private , el dominio de accesibilidad de M es el texto de programa de
T .
A partir de estas definiciones, sigue que el dominio de accesibilidad de un miembro anidado siempre es al
menos el texto del programa del tipo en el que se declara el miembro. Además, sigue que el dominio de
accesibilidad de un miembro nunca es más inclusivo que el dominio de accesibilidad del tipo en el que se
declara el miembro.
En términos intuitivos, cuando se tiene acceso a un tipo o miembro M , se evalúan los pasos siguientes para
asegurarse de que se permite el acceso:
En primer lugar, si M se declara dentro de un tipo (en lugar de una unidad de compilación o un espacio de
nombres), se produce un error en tiempo de compilación si ese tipo no es accesible.
Después, si M es public , se permite el acceso.
De lo contrario, si M es protected internal , se permite el acceso si se produce en el programa en el que M
se declara, o si se produce dentro de una clase derivada de la clase en la que M se declara y tiene lugar a
través del tipo de clase derivada (acceso protegido para miembros de instancia).
De lo contrario, si M es protected , se permite el acceso si se produce dentro de la clase en la que M se
declara, o si se produce en una clase derivada de la clase en la que M se declara y tiene lugar a través del
tipo de clase derivada (acceso protegido para miembros de instancia).
De lo contrario, si M es internal , se permite el acceso si se produce en el programa en el que M se
declara.
De lo contrario, si M es private , se permite el acceso si se produce dentro del tipo en el que M se declara.
De lo contrario, no se puede obtener acceso al tipo o miembro y se produce un error en tiempo de
compilación.
En el ejemplo
public class A
{
public static int X;
internal static int Y;
private static int Z;
}
internal class B
{
public static int X;
internal static int Y;
private static int Z;
public class C
{
public static int X;
internal static int Y;
private static int Z;
}
private class D
{
public static int X;
internal static int Y;
private static int Z;
}
}
Como se muestra en el ejemplo, el dominio de accesibilidad de un miembro nunca es mayor que el de un tipo
contenedor. Por ejemplo, aunque todos los X miembros tienen una accesibilidad declarada pública, todos pero
A.X tienen dominios de accesibilidad que están restringidos por un tipo contenedor.
Como se describe en miembros, todos los miembros de una clase base, excepto los constructores de instancias,
destructores y constructores estáticos, los heredan los tipos derivados. Esto incluye incluso miembros privados
de una clase base. Sin embargo, el dominio de accesibilidad de un miembro privado incluye solo el texto del
programa del tipo en el que se declara el miembro. En el ejemplo
class A
{
int x;
class B: A
{
static void F(B b) {
b.x = 1; // Error, x not accessible
}
}
la B clase hereda el miembro privado x de la A clase. Dado que el miembro es privado, solo es accesible
dentro del class_body de A . Por lo tanto, el acceso a se b.x realiza correctamente en el A.F método, pero se
produce un error en el B.F método.
Acceso protegido para miembros de instancia
Cuando se protected tiene acceso a un miembro de instancia fuera del texto del programa de la clase en la que
se declara, y cuando se protected internal tiene acceso a un miembro de instancia fuera del texto del
programa en el que se declara, el acceso debe realizarse dentro de una declaración de clase que deriva de la
clase en la que se declara. Además, el acceso debe realizarse a través de una instancia de ese tipo de clase
derivada o de un tipo de clase construido a partir de él. Esta restricción evita que una clase derivada tenga
acceso a miembros protegidos de otras clases derivadas, incluso cuando los miembros se heredan de la misma
clase base.
Supongamos B que es una clase base que declara un miembro M de instancia protegido y que D es una clase
que deriva de B . En el class_body de D , el acceso a M puede adoptar uno de los siguientes formatos:
Type_name o primary_expression no calificados del formulario M .
Primary_expression del formulario E.M , siempre que el tipo de E sea T o una clase derivada de T ,
donde T es el tipo de clase D o un tipo de clase construido a partir de D
Primary_expression del formulario base.M .
Además de estas formas de acceso, una clase derivada puede tener acceso a un constructor de instancia
protegido de una clase base en un constructor_initializer (inicializadores de constructor).
En el ejemplo
public class A
{
protected int x;
public class B: A
{
static void F(A a, B b) {
a.x = 1; // Error, must access through instance of B
b.x = 1; // Ok
}
}
dentro de A , es posible obtener acceso x a través de instancias de A y B , ya que en cualquier caso el
acceso se realiza a través de una instancia de A o una clase derivada de A . Sin embargo, en B , no es posible
obtener acceso x a través de una instancia de A , ya que no se A deriva de B .
En el ejemplo
class C<T>
{
protected T x;
}
las tres asignaciones a x se permiten porque todas tienen lugar a través de instancias de tipos de clase
construidas a partir del tipo genérico.
Restricciones de accesibilidad
Varias construcciones del lenguaje C# requieren que un tipo sea al menos tan accesible como miembro u
otro tipo. Se dice que un tipo T es al menos tan accesible como miembro o tipo M si el dominio de
accesibilidad de T es un supraconjunto del dominio de accesibilidad de M . En otras palabras, T es al menos
tan accesible como M si T es accesible en todos los contextos en los que M es accesible.
Existen las siguientes restricciones de accesibilidad:
La clase base directa de un tipo de clase debe ser al menos igual de accesible que el propio tipo de clase.
Las interfaces base explícitas de un tipo de interfaz deben ser al menos igual de accesibles que el propio tipo
de interfaz.
El tipo de valor devuelto y los tipos de parámetros de un tipo de delegado deben ser al menos igual de
accesibles que el propio tipo de delegado.
El tipo de una constante debe ser al menos igual de accesible que la propia constante.
El tipo de un campo debe ser al menos igual de accesible que el propio campo.
El tipo de valor devuelto y los tipos de parámetros de un método deben ser al menos igual de accesibles que
el propio método.
El tipo de una propiedad debe ser al menos igual de accesible que la misma propiedad.
El tipo de un evento debe ser al menos igual de accesible que el propio evento.
Los tipos de parámetro y el tipo de un indexador deben ser al menos igual de accesibles que el propio
indexador.
El tipo de valor devuelto y los tipos de parámetro de un operador deben ser al menos igual de accesibles que
el propio operador.
Los tipos de parámetro de un constructor de instancia deben ser al menos tan accesibles como el propio
constructor de instancia.
En el ejemplo
class A {...}
la B clase produce un error en tiempo de compilación porque A no es al menos tan accesible como B .
Del mismo modo, en el ejemplo
class A {...}
public class B
{
A F() {...}
Firmas y sobrecarga
Los métodos, los constructores de instancia, los indizadores y los operadores se caracterizan por sus firmas :
La firma de un método consta del nombre del método, el número de parámetros de tipo y el tipo y la clase
(valor, referencia o salida) de cada uno de sus parámetros formales, que se considera en el orden de
izquierda a derecha. Para estos propósitos, cualquier parámetro de tipo del método que se produce en el tipo
de un parámetro formal se identifica no por su nombre, sino por su posición ordinal en la lista de
argumentos de tipo del método. La firma de un método no incluye específicamente el tipo de valor devuelto,
el params modificador que se puede especificar para el parámetro situado más a la derecha, ni las
restricciones de parámetro de tipo opcionales.
La firma de un constructor de instancia consta del tipo y el tipo (valor, referencia o salida) de cada uno de sus
parámetros formales, considerados en el orden de izquierda a derecha. La firma de un constructor de
instancia no incluye específicamente el params modificador que se puede especificar para el parámetro
situado más a la derecha.
La firma de un indexador consta del tipo de cada uno de sus parámetros formales, que se considera en el
orden de izquierda a derecha. La firma de un indexador no incluye específicamente el tipo de elemento, ni
incluye el params modificador que se puede especificar para el parámetro situado más a la derecha.
La firma de un operador consta del nombre del operador y el tipo de cada uno de sus parámetros formales,
considerados en el orden de izquierda a derecha. La firma de un operador no incluye específicamente el tipo
de resultado.
Las firmas son el mecanismo de habilitación para la sobrecarga de miembros en clases, Structs e interfaces:
La sobrecarga de métodos permite a una clase, estructura o interfaz declarar varios métodos con el mismo
nombre, siempre que sus firmas sean únicas dentro de esa clase, estructura o interfaz.
La sobrecarga de constructores de instancia permite a una clase o struct declarar varios constructores de
instancia, siempre que sus firmas sean únicas dentro de esa clase o estructura.
La sobrecarga de indexadores permite a una clase, estructura o interfaz declarar varios indexadores, siempre
que sus firmas sean únicas dentro de esa clase, estructura o interfaz.
La sobrecarga de los operadores permite a una clase o struct declarar varios operadores con el mismo
nombre, siempre que sus firmas sean únicas dentro de esa clase o estructura.
Aunque out ref los modificadores de parámetro y se consideran parte de una firma, los miembros
declarados en un tipo único no pueden diferir en la firma únicamente en ref y out . Se produce un error en
tiempo de compilación si dos miembros se declaran en el mismo tipo con firmas que serían iguales si todos los
parámetros de ambos métodos con out Modificadores se cambiaran a ref modificadores. Para otros
propósitos de coincidencia de la firma (por ejemplo, ocultar o reemplazar), ref y out se consideran parte de
la firma y no coinciden entre sí. (Esta restricción consiste en permitir que los programas de C# se traduzcan
fácilmente para ejecutarse en el Common Language Infrastructure (CLI), que no proporciona una manera de
definir métodos que difieren únicamente en ref y out ).
En el caso de las firmas, los tipos object y dynamic se consideran iguales. Por lo tanto, los miembros
declarados en un tipo único pueden no diferir en la firma únicamente en object y dynamic .
En el ejemplo siguiente se muestra un conjunto de declaraciones de método sobrecargado junto con sus firmas.
interface ITest
{
void F(); // F()
Tenga en cuenta que todos los ref out modificadores de parámetro y (parámetros de método) forman parte
de una firma. Por lo tanto, F(int) y F(ref int) son firmas únicas. Sin embargo, F(ref int) y F(out int) no
se pueden declarar dentro de la misma interfaz porque sus firmas difieren únicamente en ref y out . Además,
tenga en cuenta que el tipo de valor devuelto y el params modificador no forman parte de una firma, por lo que
no es posible sobrecargar únicamente en función del tipo de valor devuelto o de la inclusión o exclusión del
params modificador. Como tal, las declaraciones de los métodos F(int) e F(params string[]) identificados
anteriormente provocan un error en tiempo de compilación.
Ámbitos
El *ámbito _ de un nombre es la región del texto del programa en la que es posible hacer referencia a la entidad
declarada por el nombre sin la calificación del nombre. Los ámbitos se pueden anidar y un ámbito interno
puede volver a declarar el significado de un nombre desde un ámbito externo (sin embargo, no se quita la
restricción impuesta por las declaraciones que se encuentran dentro de un bloque anidado no es posible
declarar una variable local con el mismo nombre que una variable local en un bloque de inclusión). A
continuación, se dice que el nombre del ámbito externo es _ Hidden* en la región del texto del programa que
abarca el ámbito interno, y el acceso al nombre exterior solo es posible mediante la calificación del nombre.
El ámbito de un miembro de espacio de nombres declarado por un namespace_member_declaration
(miembros de espacio de nombres) sin ningún namespace_declaration envolvente es todo el texto del
programa.
El ámbito de un miembro de espacio de nombres declarado por una namespace_member_declaration dentro
de una namespace_declaration cuyo nombre completo es N el namespace_body de cada
namespace_declaration cuyo nombre completo es N o comienza por N , seguido de un punto.
El ámbito del nombre definido por una extern_alias_directive se extiende por las using_directive s,
global_attributes y namespace_member_declaration s de su unidad de compilación o cuerpo de espacio de
nombres que contiene inmediatamente. Un extern_alias_directive no aporta ningún miembro nuevo al
espacio de declaración subyacente. En otras palabras, una extern_alias_directive no es transitiva, sino que
afecta solo a la unidad de compilación o al cuerpo del espacio de nombres en el que se produce.
El ámbito de un nombre definido o importado por un using_directive (mediante directivas) se extiende por
los namespace_member_declaration s del compilation_unit o namespace_body en el que se produce el
using_directive . Un using_directive puede hacer que cero o más nombres de espacio de nombres, tipo o
miembro estén disponibles dentro de un compilation_unit o namespace_body determinado, pero no aporta
ningún miembro nuevo al espacio de declaración subyacente. En otras palabras, una using_directive no es
transitiva sino que solo afecta al compilation_unit o namespace_body en el que se produce.
El ámbito de un parámetro de tipo declarado por un type_parameter_list en un class_declaration
(declaraciones de clase) es el class_base, type_parameter_constraints_clause s y class_body de ese
class_declaration.
El ámbito de un parámetro de tipo declarado por un type_parameter_list en una struct_declaration
(declaraciones de struct) es el struct_interfaces, type_parameter_constraints_clause s y struct_body de ese
struct_declaration.
El ámbito de un parámetro de tipo declarado por un type_parameter_list en una interface_declaration
(declaraciones de interfaz) es el interface_base, type_parameter_constraints_clause s y interface_body de ese
interface_declaration.
El ámbito de un parámetro de tipo declarado por un type_parameter_list en una delegate_declaration
(declaraciones de delegado) es el return_type, formal_parameter_list y type_parameter_constraints_clause s
de ese delegate_declaration.
El ámbito de un miembro declarado por un class_member_declaration (cuerpo de clase) es el class_body en
el que se produce la declaración. Además, el ámbito de un miembro de clase se extiende al class_body de las
clases derivadas que se incluyen en el dominio de accesibilidad (dominios de accesibilidad) del miembro.
El ámbito de un miembro declarado por un struct_member_declaration (miembros de struct) es el
struct_body en el que se produce la declaración.
El ámbito de un miembro declarado por un enum_member_declaration (enumerar miembros) es el
enum_body en el que se produce la declaración.
El ámbito de un parámetro declarado en una method_declaration (métodos) es el method_body de ese
method_declaration.
El ámbito de un parámetro declarado en una indexer_declaration (indizadores) es el accessor_declarations de
ese indexer_declaration.
El ámbito de un parámetro declarado en una operator_declaration (operadores) es el bloque de ese
operator_declaration.
El ámbito de un parámetro declarado en una constructor_declaration (constructores de instancia) es el
constructor_initializer y el bloque de ese constructor_declaration.
El ámbito de un parámetro declarado en una lambda_expression (expresiones de función anónimas) es el
anonymous_function_body de ese lambda_expression
El ámbito de un parámetro declarado en una anonymous_method_expression (expresiones de función
anónimas) es el bloque de ese anonymous_method_expression.
El ámbito de una etiqueta declarada en un labeled_statement (instrucciones con etiqueta) es el bloque en el
que se produce la declaración.
El ámbito de una variable local declarada en un local_variable_declaration (declaraciones de variables locales)
es el bloque en el que se produce la declaración.
El ámbito de una variable local declarada en un switch_block de una switch instrucción (la instrucción
switch) es el switch_block.
El ámbito de una variable local declarada en un for_initializer de una for instrucción (la instrucción for) es el
for_initializer, el for_condition, el for_iterator y la instrucción contenida de la for instrucción.
El ámbito de una constante local declarada en un local_constant_declaration (declaraciones de constantes
locales) es el bloque en el que se produce la declaración. Es un error en tiempo de compilación hacer
referencia a una constante local en una posición textual que precede a su constant_declarator.
El ámbito de una variable declarada como parte de un foreach_statement, using_statement, lock_statement o
query_expression viene determinado por la expansión de la construcción especificada.
Dentro del ámbito de un espacio de nombres, una clase, un struct o un miembro de enumeración, es posible
hacer referencia al miembro en una posición textual que preceda a la declaración del miembro. Por ejemplo
class A
{
void F() {
i = 1;
}
int i = 0;
}
class A
{
int i = 0;
void F() {
i = 1; // Error, use precedes declaration
int i;
i = 2;
}
void G() {
int j = (j = 1); // Valid
}
void H() {
int a = 1, b = ++a; // Valid
}
}
using System;
class A {}
class Test
{
static void Main() {
string A = "hello, world";
string s = A; // expression context
el nombre A se usa en un contexto de expresión para hacer referencia a la variable local A y en un contexto de
tipo para hacer referencia a la clase A .
Ocultación de nombres
El ámbito de una entidad suele abarcar más texto del programa que el espacio de declaración de la entidad. En
concreto, el ámbito de una entidad puede incluir declaraciones que introducen nuevos espacios de declaración
que contienen entidades con el mismo nombre. Dichas declaraciones hacen que la entidad original se convierta
en *Hidden _. Por el contrario, se dice que una entidad es _ visible* cuando no está oculta.
La ocultación de nombres se produce cuando los ámbitos se superponen mediante anidamiento y cuando los
ámbitos se superponen a través de la herencia. En las secciones siguientes se describen las características de los
dos tipos de ocultación.
Ocultar mediante anidamiento
La ocultación de nombres mediante el anidamiento puede producirse como resultado de anidar espacios de
nombres o tipos dentro de los espacios de nombres, como resultado de anidar tipos dentro de clases o Structs, y
como resultado de parámetros y declaraciones de variables locales.
En el ejemplo
class A
{
int i = 0;
void F() {
int i = 1;
}
void G() {
i = 1;
}
}
dentro del F método, la variable de instancia i está oculta por la variable local i , pero dentro del G
método i todavía hace referencia a la variable de instancia.
Cuando un nombre de un ámbito interno oculta un nombre en un ámbito externo, oculta todas las apariciones
sobrecargadas de ese nombre. En el ejemplo
class Outer
{
static void F(int i) {}
class Inner
{
void G() {
F(1); // Invokes Outer.Inner.F
F("Hello"); // Error
}
la llamada F(1) invoca el F declarado en Inner porque todas las repeticiones externas de F están ocultas
por la declaración interna. Por la misma razón, la llamada F("Hello") produce un error en tiempo de
compilación.
Ocultar a través de la herencia
La ocultación de nombres a través de la herencia se produce cuando las clases o Structs declaran nombres que
se heredaron de clases base. Este tipo de ocultación de nombres adopta uno de los siguientes formatos:
Una constante, un campo, una propiedad, un evento o un tipo introducidos en una clase o struct oculta todos
los miembros de clase base con el mismo nombre.
Un método introducido en una clase o struct oculta todos los miembros de clase base que no son de método
con el mismo nombre y todos los métodos de clase base con la misma firma (nombre de método y número
de parámetros, modificadores y tipos).
Un indizador introducido en una clase o struct oculta todos los indizadores de clase base con la misma firma
(número de parámetros y tipos).
Las reglas que rigen las declaraciones de operador (operadores) hacen imposible que una clase derivada declare
un operador con la misma signatura que un operador en una clase base. Por lo tanto, los operadores nunca se
ocultan entre sí.
Al contrario que ocultar un nombre de un ámbito externo, si se oculta un nombre accesible desde un ámbito
heredado, se genera una advertencia. En el ejemplo
class Base
{
public void F() {}
}
la declaración de F en Derived produce una advertencia que se va a informar. Ocultar un nombre heredado
no es específicamente un error, ya que esto impedirá la evolución independiente de las clases base. Por ejemplo,
la situación anterior podría haber surgido debido a que una versión posterior de Base presentó un F método
que no estaba presente en una versión anterior de la clase. Si la situación anterior hubiera sido un error,
cualquier cambio realizado en una clase base en una biblioteca de clases con versiones independientes podría
provocar que las clases derivadas dejen de ser válidas.
La advertencia causada por ocultar un nombre heredado puede eliminarse mediante el uso del new
modificador:
class Base
{
public void F() {}
}
El new modificador indica que F en Derived es "nuevo" y que, en realidad, está diseñado para ocultar el
miembro heredado.
Una declaración de un nuevo miembro oculta un miembro heredado solo dentro del ámbito del nuevo
miembro.
class Base
{
public static void F() {}
}
En el ejemplo anterior, la declaración de F en Derived oculta el F objeto heredado de Base , pero como el
nuevo F en Derived tiene acceso privado, su ámbito no se extiende a MoreDerived . Por lo tanto, la llamada
F() en MoreDerived.G es válida y llamará a Base.F .
namespace_name
: namespace_or_type_name
;
type_name
: namespace_or_type_name
;
namespace_or_type_name
: identifier type_argument_list?
| namespace_or_type_name '.' identifier type_argument_list?
| qualified_alias_member
;
Nombres completos
Cada espacio de nombres y tipo tiene un nombre completo , que identifica de forma única el espacio de
nombres o el tipo entre todos los demás. El nombre completo de un espacio de nombres o tipo N se determina
de la siguiente manera:
Si N es miembro del espacio de nombres global, su nombre completo es N .
De lo contrario, su nombre completo es S.N , donde S es el nombre completo del espacio de nombres o
tipo en el que N se declara.
En otras palabras, el nombre completo de N es la ruta de acceso jerárquica completa de los identificadores que
conducen a, a partir del N espacio de nombres global. Dado que cada miembro de un espacio de nombres o
tipo debe tener un nombre único, sigue que el nombre completo de un espacio de nombres o tipo es siempre
único.
En el ejemplo siguiente se muestran varias declaraciones de espacio de nombres y tipo junto con sus nombres
completos asociados.
class A {} // A
namespace X // X
{
class B // X.B
{
class C {} // X.B.C
}
namespace Y // X.Y
{
class D {} // X.Y.D
}
}
using System;
class A
{
~A() {
Console.WriteLine("Destruct instance of A");
}
}
class B
{
object Ref;
public B(object o) {
Ref = o;
}
~B() {
Console.WriteLine("Destruct instance of B");
}
}
class Test
{
static void Main() {
B b = new B(new A());
b = null;
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
crea una instancia de la clase A y una instancia de la clase B . Estos objetos son válidos para la recolección de
elementos no utilizados cuando b se asigna el valor a la variable null , ya que no es posible que ningún
código escrito por el usuario tenga acceso a ellos. El resultado puede ser
Destruct instance of A
Destruct instance of B
or
Destruct instance of B
Destruct instance of A
Dado que el lenguaje no impone ninguna restricción en el orden en el que los objetos se recolectan como
elementos no utilizados.
En casos sutiles, la distinción entre "candidato a la destrucción" y "puede ser válida para la recopilación" puede
ser importante. Por ejemplo,
using System;
class A
{
~A() {
Console.WriteLine("Destruct instance of A");
}
class B
{
public A Ref;
~B() {
Console.WriteLine("Destruct instance of B");
Ref.F();
}
}
class Test
{
public static A RefA;
public static B RefB;
En el programa anterior, si el recolector de elementos no utilizados elige ejecutar el destructor de A antes del
destructor de B , el resultado de este programa podría ser:
Destruct instance of A
Destruct instance of B
A.F
RefA is not null
Tenga en cuenta que aunque la instancia de A no estaba en uso y A se ejecutó el destructor de, todavía es
posible A llamar a los métodos de (en este caso, F ) desde otro destructor. Además, tenga en cuenta que la
ejecución de un destructor puede hacer que un objeto se pueda utilizar de nuevo desde el programa principal.
En este caso, la ejecución del B destructor de ha provocado que una instancia de A que anteriormente no
estaba en uso sea accesible desde la referencia activa Test.RefA . Después de la llamada a
WaitForPendingFinalizers , la instancia de B es válida para la recolección, pero la instancia de A no es, debido
a la referencia Test.RefA .
Para evitar la confusión y el comportamiento inesperado, suele ser una buena idea que los destructores solo
realicen la limpieza en los datos almacenados en sus propios campos del objeto y no realicen ninguna acción en
los objetos a los que se hace referencia ni en los campos estáticos.
Una alternativa al uso de destructores es dejar que una clase implemente la System.IDisposable interfaz. Esto
permite al cliente del objeto determinar cuándo liberar los recursos del objeto, normalmente mediante el acceso
al objeto como un recurso en una using instrucción (la instrucción using).
Orden de ejecución
La ejecución de un programa de C# continúa de tal forma que los efectos secundarios de cada subproceso en
ejecución se conserven en puntos de ejecución críticos. Un efecto secundario se define como una lectura o
escritura de un campo volátil, una escritura en una variable no volátil, una escritura en un recurso externo y el
inicio de una excepción. Los puntos de ejecución críticos en los que se debe conservar el orden de estos efectos
secundarios son las referencias a campos volátiles (campos volátiles), las lock instrucciones (instrucción lock) y
la creación y terminación de subprocesos. El entorno de ejecución es gratuito para cambiar el orden de
ejecución de un programa de C#, sujeto a las siguientes restricciones:
La dependencia de los datos se conserva dentro de un subproceso de ejecución. Es decir, el valor de cada
variable se calcula como si todas las instrucciones del subproceso se ejecutaran en el orden del programa
original.
Se conservan las reglas de ordenación de inicialización (inicialización de campos e inicializadores de
variables).
El orden de los efectos secundarios se conserva con respecto a las lecturas y escrituras volátiles (campos
volátiles). Además, el entorno de ejecución no necesita evaluar parte de una expresión si puede deducir que
el valor de esa expresión no se usa y que no se producen efectos secundarios necesarios (incluidos los
causados por una llamada a un método o el acceso a un campo volátil). Cuando se interrumpe la ejecución
del programa mediante un evento asincrónico (como una excepción iniciada por otro subproceso), no se
garantiza que los efectos secundarios observables estén visibles en el orden del programa original.
Tipos
18/09/2021 • 71 minutes to read
Los tipos del lenguaje C# se dividen en dos categorías principales: tipos de valor _ y _tipos de referencia*.
Ambos tipos de valor y tipos de referencia pueden ser tipos genéricos, que toman uno o varios parámetros de
tipo _ * * *. Los parámetros de tipo pueden designar tanto tipos de valor como tipos de referencia.
type
: value_type
| reference_type
| type_parameter
| type_unsafe
;
La categoría final de tipos, punteros, solo está disponible en código no seguro. Esto se describe con más detalle
en tipos de puntero.
Los tipos de valor se diferencian de los tipos de referencia en que las variables de los tipos de valor contienen
directamente sus datos, mientras que las variables de los tipos de referencia almacenan referencias en sus
datos, lo que se conoce como "objetos *". Con los tipos de referencia, es posible que dos variables hagan
referencia al mismo objeto y, por lo tanto, las operaciones en una variable afecten al objeto al que hace
referencia la otra variable. Con los tipos de valor, cada variable tiene su propia copia de los datos y no es posible
que las operaciones en una afecten a la otra.
El sistema de tipos de C# está unificado, de modo que un valor de cualquier tipo se puede tratar como un
objeto. Todos los tipos de C# directa o indirectamente se derivan del tipo de clase object , y object es la clase
base definitiva de todos los tipos. Los valores de tipos de referencia se tratan como objetos mediante la
visualización de los valores como tipo object . Los valores de los tipos de valor se tratan como objetos
realizando las operaciones de conversión boxing y unboxing (conversión boxing y conversión unboxing).
Tipos de valor
Un tipo de valor es un tipo de estructura o un tipo de enumeración. C# proporciona un conjunto de tipos de
struct predefinidos denominados tipos simples . Los tipos simples se identifican mediante palabras reservadas.
value_type
: struct_type
| enum_type
;
struct_type
: type_name
| simple_type
| nullable_type
;
simple_type
: numeric_type
| 'bool'
;
numeric_type
: integral_type
| floating_point_type
| 'decimal'
;
integral_type
: 'sbyte'
| 'byte'
| 'short'
| 'ushort'
| 'int'
| 'uint'
| 'long'
| 'ulong'
| 'char'
;
floating_point_type
: 'float'
| 'double'
;
nullable_type
: non_nullable_value_type '?'
;
non_nullable_value_type
: type
;
enum_type
: type_name
;
A diferencia de una variable de un tipo de referencia, una variable de un tipo de valor solo puede contener el
valor null si el tipo de valor es un tipo que acepta valores NULL. Para cada tipo de valor que no acepta valores
NULL, hay un tipo de valor que acepta valores NULL correspondiente que denota el mismo conjunto de valores
más el valor null .
La asignación a una variable de un tipo de valor crea una copia del valor que se va a asignar. Esto difiere de la
asignación a una variable de un tipo de referencia, que copia la referencia pero no el objeto identificado por la
referencia.
Tipo System. ValueType
Todos los tipos de valor heredan implícitamente de la clase System.ValueType , que, a su vez, hereda de la clase
object . No es posible que ningún tipo derive de un tipo de valor y, por tanto, los tipos de valor están sellados
implícitamente (clases selladas).
Tenga en cuenta que System.ValueType no es un value_type. En su lugar, es una class_type de la que se derivan
automáticamente todos los value_type s.
Constructores predeterminados
Todos los tipos de valor declaran implícitamente un constructor de instancia sin parámetros público
denominado *constructor predeterminado _. El constructor predeterminado devuelve una instancia
inicializada en cero, conocida como el valor _ default* para el tipo de valor:
Para todos los Simple_Type s, el valor predeterminado es el valor generado por un patrón de bits de todos
los ceros:
Para sbyte , byte , short , ushort , int , uint , long y ulong , el valor predeterminado es 0 .
En char , el valor predeterminado es '\x0000' .
En float , el valor predeterminado es 0.0f .
En double , el valor predeterminado es 0.0d .
En decimal , el valor predeterminado es 0.0m .
En bool , el valor predeterminado es false .
Para un enum_type E , el valor predeterminado es 0 , convertido al tipo E .
Para un struct_type, el valor predeterminado es el valor generado al establecer todos los campos de tipo de
valor en sus valores predeterminados y todos los campos de tipo de referencia en null .
Para un nullable_type el valor predeterminado es una instancia para la que la HasValue propiedad es false y
la Value propiedad no está definida. El valor predeterminado también se conoce como el valor null del tipo
que acepta valores NULL.
Al igual que cualquier otro constructor de instancia, el constructor predeterminado de un tipo de valor se invoca
mediante el new operador. Por motivos de eficacia, este requisito no está diseñado para tener realmente la
implementación que genera una llamada al constructor. En el ejemplo siguiente, las variables i y j se
inicializan en cero.
class A
{
void F() {
int i = 0;
int j = new int();
}
}
Dado que cada tipo de valor tiene implícitamente un constructor de instancia sin parámetros público, no es
posible que un tipo de struct contenga una declaración explícita de un constructor sin parámetros. Sin embargo,
se permite que un tipo de estructura declare constructores de instancia con parámetros (constructores).
Tipos de estructura
Un tipo de estructura es un tipo de valor que puede declarar constantes, campos, métodos, propiedades,
indizadores, operadores, constructores de instancias, constructores estáticos y tipos anidados. La declaración de
tipos de struct se describe en declaraciones de struct.
Tipos simples
C# proporciona un conjunto de tipos de struct predefinidos denominados tipos simples . Los tipos simples se
identifican mediante palabras reservadas, pero estas palabras reservadas son simplemente alias para los tipos
de struct predefinidos en el System espacio de nombres, tal como se describe en la tabla siguiente.
PA L A B RA RESERVA DA T IP O C O N A L IA S
sbyte System.SByte
byte System.Byte
short System.Int16
ushort System.UInt16
int System.Int32
uint System.UInt32
long System.Int64
ulong System.UInt64
char System.Char
float System.Single
double System.Double
bool System.Boolean
decimal System.Decimal
Dado que un tipo simple incluye un alias para un tipo de estructura, cada tipo simple tiene miembros. Por
ejemplo, int tiene los miembros declarados en System.Int32 y los miembros heredados de System.Object , y
se permiten las siguientes instrucciones:
Los tipos simples se diferencian de otros tipos struct en que permiten determinadas operaciones adicionales:
La mayoría de los tipos simples permiten crear valores escribiendo literales (literales). Por ejemplo, 123 es
un literal de tipo int y 'a' es un literal de tipo char . C# no hace ningún aprovisionamiento de los
literales de tipos struct en general, y los valores no predeterminados de otros tipos struct siempre se crean
en última instancia mediante constructores de instancias de esos tipos de estructura.
Cuando los operandos de una expresión son constantes de tipo simple, es posible que el compilador evalúe
la expresión en tiempo de compilación. Este tipo de expresión se conoce como constant_expression
(expresiones constantes). Las expresiones que implican operadores definidos por otros tipos struct no se
consideran expresiones constantes.
A través const de las declaraciones, es posible declarar constantes de los tipos simples (constantes). No es
posible tener constantes de otros tipos struct, pero los campos proporcionan un efecto similar
static readonly .
Las conversiones que implican tipos simples pueden participar en la evaluación de operadores de conversión
definidos por otros tipos de struct, pero un operador de conversión definido por el usuario nunca puede
participar en la evaluación de otro operador definido por el usuario (evaluación de conversiones definidas
por el usuario).
Tipos enteros
C# admite nueve tipos enteros: sbyte , byte , short , ushort , int , uint , long , ulong y char . Los tipos
enteros tienen los siguientes tamaños y rangos de valores:
El sbyte tipo representa enteros de 8 bits con signo con valores comprendidos entre-128 y 127.
El byte tipo representa enteros de 8 bits sin signo con valores comprendidos entre 0 y 255.
El short tipo representa enteros de 16 bits con signo con valores comprendidos entre-32768 y 32767.
El ushort tipo representa enteros de 16 bits sin signo con valores comprendidos entre 0 y 65535.
El int tipo representa enteros de 32 bits con signo con valores comprendidos entre-2147483648 y
2147483647.
El uint tipo representa enteros de 32 bits sin signo con valores comprendidos entre 0 y 4294967295.
El long tipo representa enteros de 64 bits con signo con valores comprendidos entre-
9223372036854775808 y 9223372036854775807.
El ulong tipo representa enteros de 64 bits sin signo con valores comprendidos entre 0 y
18446744073709551615.
El char tipo representa enteros de 16 bits sin signo con valores comprendidos entre 0 y 65535. El conjunto
de valores posibles para el tipo char corresponde al juego de caracteres Unicode. Aunque char tiene la
misma representación que ushort , no todas las operaciones permitidas en un tipo se permiten en el otro.
Los operadores unarios y binarios de tipo entero siempre operan con una precisión de 32 bits con signo, una
precisión de 32 bits sin signo, una precisión de 64 con signo o una precisión de bit 64 sin signo:
En el caso de los operadores unario + y ~ , el operando se convierte al tipo T , donde T es el primero de
int ,, uint long y ulong que puede representar completamente todos los valores posibles del operando.
La operación se realiza entonces con la precisión del tipo T y el tipo del resultado es T .
Para el operador unario - , el operando se convierte al tipo T , donde T es el primero de int y long
que puede representar completamente todos los valores posibles del operando. La operación se realiza
entonces con la precisión del tipo T y el tipo del resultado es T . El operador unario - no se puede aplicar
a operandos de tipo ulong .
En el caso de los operadores binarios + ,,,,,,,,,, - * / % & ^ | == != > , < , y >= <= , los
operandos se convierten al tipo T , donde T es el primero de int , uint , y, long ulong que puede
representar por completo todos los valores posibles de ambos operandos. La operación se realiza entonces
con la precisión del tipo T y el tipo del resultado es T (o bool para los operadores relacionales). No se
permite que un operando sea de tipo long y el otro para ser de tipo ulong con los operadores binarios.
En el caso de los operadores binarios << y >> , el operando izquierdo se convierte al tipo T , donde T es
el primero de int ,, y, uint long ulong que puede representar completamente todos los valores posibles
del operando. La operación se realiza entonces con la precisión del tipo T y el tipo del resultado es T .
El char tipo se clasifica como un tipo entero, pero difiere de los demás tipos enteros de dos maneras:
No hay ninguna conversión implícita de otros tipos al tipo char . En concreto, aunque los sbyte tipos, byte
y ushort tienen intervalos de valores que se pueden representar completamente con el char tipo, las
conversiones implícitas de sbyte , byte o ushort char no existen.
Las constantes del char tipo deben escribirse como character_literal s o como integer_literal s en
combinación con una conversión al tipo char . Por ejemplo, (char)10 es lo mismo que '\x000A' .
Los checked unchecked operadores and y las instrucciones se utilizan para controlar la comprobación de
desbordamiento de las operaciones aritméticas de tipo entero y las conversiones (los operadores Checked y
unchecked). En un checked contexto, un desbordamiento produce un error en tiempo de compilación o provoca
System.OverflowExceptionque se produzca una excepción. En un unchecked contexto, se omiten los
desbordamientos y se descartan los bits de orden superior que no caben en el tipo de destino.
Tipos de punto flotante
C# admite dos tipos de punto flotante: float y double . Los float double tipos y se representan mediante
los formatos IEEE 754 de precisión sencilla de 32 bits y de doble precisión de 64 bits, que proporcionan los
siguientes conjuntos de valores:
Cero positivo y cero negativo. En la mayoría de los casos, cero positivo y cero negativo se comportan
exactamente igual que el valor cero simple, pero ciertas operaciones distinguen entre los dos (operador de
división).
Infinito positivo y infinito negativo. Los infinitos se generan mediante operaciones como la división por cero
de un número distinto de cero. Por ejemplo, 1.0 / 0.0 produce infinito positivo y -1.0 / 0.0 produce
infinito negativo.
El valor no numérico , a menudo abreviado como Nan. Los Nan se generan mediante operaciones de punto
flotante no válidas, como la división de cero por cero.
El conjunto finito de valores distintos de cero del formulario s * m * 2^e , donde s es 1 o-1, y m y e
vienen determinados por el tipo de punto flotante determinado: para float , 0 < m < 2^24 y
-149 <= e <= 104 , y para double , 0 < m < 2^53 y -1075 <= e <= 970 . Los números de punto flotante
desnormalizados se consideran valores distintos de cero válidos.
El float tipo puede representar valores comprendidos entre aproximadamente 1.5 * 10^-45 y 3.4 * 10^38
con una precisión de 7 dígitos.
El double tipo puede representar valores comprendidos entre aproximadamente 5.0 * 10^-324 y
1.7 × 10^308 con una precisión de 15-16 dígitos.
Si uno de los operandos de un operador binario es de un tipo de punto flotante, el otro operando debe ser de
un tipo entero o de un tipo de punto flotante, y la operación se evalúa como sigue:
Si uno de los operandos es de un tipo entero, ese operando se convierte en el tipo de punto flotante del otro
operando.
Después, si alguno de los operandos es de tipo double , el otro operando se convierte en double , la
operación se realiza utilizando al menos el double intervalo y la precisión, y el tipo del resultado es double
(o bool para los operadores relacionales).
De lo contrario, la operación se realiza utilizando al menos el float intervalo y la precisión, y el tipo del
resultado es float (o bool para los operadores relacionales).
Los operadores de punto flotante, incluidos los operadores de asignación, nunca generan excepciones. En su
lugar, en situaciones excepcionales, las operaciones de punto flotante producen cero, infinito o NaN, como se
describe a continuación:
Si el resultado de una operación de punto flotante es demasiado pequeño para el formato de destino, el
resultado de la operación se convierte en cero positivo o negativo.
Si el resultado de una operación de punto flotante es demasiado grande para el formato de destino, el
resultado de la operación se convierte en infinito positivo o infinito negativo.
Si una operación de punto flotante no es válida, el resultado de la operación se convierte en NaN.
Si uno o los dos operandos de una operación de punto flotante son NaN, el resultado de la operación se
convierte en NaN.
Las operaciones de punto flotante se pueden realizar con una precisión mayor que el tipo de resultado de la
operación. Por ejemplo, algunas arquitecturas de hardware admiten un tipo de punto flotante "extendido" o
"Long Double" con un intervalo y una precisión mayores que el double tipo y realizan implícitamente todas las
operaciones de punto flotante con este tipo de precisión superior. Solo con un costo excesivo en el rendimiento
se pueden realizar estas arquitecturas de hardware para realizar operaciones de punto flotante con menos
precisión y, en lugar de requerir una implementación para que se pierda rendimiento y precisión, C# permite
usar un tipo de precisión mayor para todas las operaciones de punto flotante. Aparte de ofrecer resultados más
precisos, esto rara vez tiene efectos medibles. Sin embargo, en las expresiones con el formato x * y / z ,
donde la multiplicación genera un resultado que está fuera del double intervalo, pero la división posterior
devuelve el resultado temporal al double intervalo, el hecho de que la expresión se evalúe en un formato de
intervalo más alto puede provocar que se genere un resultado finito en lugar de un infinito.
El tipo decimal
El tipo decimal es un tipo de datos de 128 bits adecuado para cálculos financieros y monetarios. El decimal
tipo puede representar valores comprendidos entre 1.0 * 10^-28 y aproximadamente 7.9 * 10^28 con 28-29
dígitos significativos.
El conjunto finito de valores de tipo decimal tiene el formato (-1)^s * c * 10^-e , donde el signo s es 0 o 1,
el coeficiente c lo proporciona 0 <= *c* < 2^96 y la escala e es tal que 0 <= e <= 28 . El decimal tipo no
admite ceros con signo, infinitos o Nan. Un decimal se representa como un entero de 96 bits escalado por una
potencia de diez. Para decimal s con un valor absoluto menor que 1.0m , el valor es exacto hasta la posición
decimal 28, pero no más. Para decimal s con un valor absoluto mayor o igual que 1.0m , el valor es exacto a 28
o 29 dígitos. Al contrario que float los double tipos de datos y, los números fraccionarios decimales como 0,1
se pueden representar exactamente en la decimal representación. En las float double representaciones y,
estos números suelen ser fracciones infinitas, por lo que las representaciones son más propensas a errores de
redondeo.
Si uno de los operandos de un operador binario es de tipo decimal , el otro operando debe ser de un tipo
entero o de tipo decimal . Si hay un operando de tipo entero, se convierte en decimal antes de que se realice la
operación.
El resultado de una operación con valores de tipo decimal es que resultaría de calcular un resultado exacto
(conservando la escala, tal y como se define para cada operador) y, a continuación, redondear para ajustarse a la
representación. Los resultados se redondean al valor representable más cercano y, cuando un resultado está
igualmente cerca de dos valores representables, al valor que tiene un número par en la posición del dígito
menos significativo (esto se conoce como "redondeo bancario"). Un resultado de cero siempre tiene un signo de
0 y una escala de 0.
Si una operación aritmética decimal produce un valor menor o igual que 5 * 10^-29 en valor absoluto, el
resultado de la operación se convierte en cero. Si una decimal operación aritmética genera un resultado que es
demasiado grande para el decimal formato, System.OverflowException se produce una excepción.
El decimal tipo tiene mayor precisión pero menor que los tipos de punto flotante. Por lo tanto, las conversiones
de los tipos de punto flotante a decimal pueden producir excepciones de desbordamiento, y las conversiones
de decimal a los tipos de punto flotante podrían provocar la pérdida de precisión. Por estos motivos, no existe
ninguna conversión implícita entre los tipos de punto flotante y decimal , y sin conversiones explícitas, no es
posible mezclar los operandos y el punto flotante decimal en la misma expresión.
Tipo bool
El bool tipo representa las cantidades lógicas booleanas. Los valores posibles de tipo bool son true y false
.
No existe ninguna conversión estándar entre bool y otros tipos. En concreto, el bool tipo es distinto e
independiente de los tipos enteros, y bool no se puede usar un valor en lugar de un valor entero, y viceversa.
En los lenguajes C y C++, un valor entero o de punto flotante cero, o un puntero nulo se puede convertir al
valor booleano false , y un valor entero distinto de cero o de punto flotante, o un puntero no nulo se puede
convertir al valor booleano true . En C#, estas conversiones se realizan comparando explícitamente un valor
entero o de punto flotante en cero, o comparando explícitamente una referencia de objeto a null .
Tipos de enumeración
Un tipo de enumeración es un tipo distinto con constantes con nombre. Cada tipo de enumeración tiene un tipo
subyacente, que debe ser byte , sbyte , short , ushort , int , uint long o ulong . El conjunto de valores
del tipo de enumeración es el mismo que el conjunto de valores del tipo subyacente. Los valores del tipo de
enumeración no están restringidos a los valores de las constantes con nombre. Los tipos de enumeración se
definen mediante declaraciones de enumeración (declaracionesde enumeración).
Tipos que aceptan valores NULL
Un tipo que acepta valores NULL puede representar todos los valores de su tipo subyacente más un valor null
adicional. Se escribe un tipo que acepta valores NULL T? , donde T es el tipo subyacente. Esta sintaxis es una
abreviatura de System.Nullable<T> y las dos formas se pueden usar indistintamente.
Un tipo de valor que no acepta valores NULL es un tipo de valor distinto de System.Nullable<T> y su
abreviatura T? (para cualquier T ), además de cualquier parámetro de tipo restringido para ser un tipo de
valor que no acepta valores NULL (es decir, cualquier parámetro de tipo con una struct restricción). El
System.Nullable<T> tipo especifica la restricción de tipo de valor para T (restricciones de parámetro de tipo), lo
que significa que el tipo subyacente de un tipo que acepta valores NULL puede ser cualquier tipo de valor que
no acepte valores NULL. El tipo subyacente de un tipo que acepta valores NULL no puede ser un tipo que acepta
valores NULL ni un tipo de referencia. Por ejemplo, int?? y string? son tipos no válidos.
Una instancia de un tipo que acepta valores NULL T? tiene dos propiedades públicas de solo lectura:
Una HasValue propiedad de tipo bool
Una Value propiedad de tipo T
Una instancia para la que HasValue es true se dice que no es NULL. Una instancia que no es null contiene un
valor conocido y Value devuelve ese valor.
Una instancia para la que HasValue es false se dice que es NULL. Una instancia null tiene un valor sin definir. Al
intentar leer la Value de una instancia null, System.InvalidOperationException se produce una excepción. El
proceso de acceso a la Value propiedad de una instancia que acepta valores NULL se conoce como
desencapsulado .
Además del constructor predeterminado, todos los tipos que aceptan valores NULL T? tienen un constructor
público que toma un único argumento de tipo T . Dado un valor x de tipo T , una invocación del constructor
con el formato
new T?(x)
crea una instancia no NULL de T? para la que la Value propiedad es x . El proceso de creación de una
instancia no NULL de un tipo que acepta valores NULL para un valor determinado se conoce como ajuste .
Las conversiones implícitas están disponibles desde el null literal a T? (conversiones de literales null) y de T
a T? (conversiones implícitas que aceptan valores NULL).
Tipos de referencia
Un tipo de referencia es un tipo de clase, un tipo de interfaz, un tipo de matriz o un tipo de delegado.
reference_type
: class_type
| interface_type
| array_type
| delegate_type
;
class_type
: type_name
| 'object'
| 'dynamic'
| 'string'
;
interface_type
: type_name
;
array_type
: non_array_type rank_specifier+
;
non_array_type
: type
;
rank_specifier
: '[' dim_separator* ']'
;
dim_separator
: ','
;
delegate_type
: type_name
;
Un valor de tipo de referencia es una referencia a una *instancia de del tipo, que se conoce como un objeto _ *
* *. El valor especial null es compatible con todos los tipos de referencia e indica la ausencia de una instancia.
Tipos de clase
Un tipo de clase define una estructura de datos que contiene miembros de datos (constantes y campos),
miembros de función (métodos, propiedades, eventos, indizadores, operadores, constructores de instancias,
destructores y constructores estáticos) y tipos anidados. Los tipos de clase admiten la herencia, un mecanismo
por el que las clases derivadas pueden extender y especializar clases base. Las instancias de tipos de clase se
crean mediante object_creation_expression s (expresiones de creación de objetos).
Los tipos de clase se describen en clases.
Algunos tipos de clase predefinidos tienen un significado especial en el lenguaje C#, tal y como se describe en la
tabla siguiente.
T IP O DE C L A SE DESC RIP C IÓ N
System.Object Última clase base de todos los demás tipos. Vea el tipo de
objeto.
System.ValueType Clase base de todos los tipos de valor. Vea el tipo System.
ValueType.
El tipo de objeto
El object tipo de clase es la clase base definitiva de todos los demás tipos. Cada tipo de C# deriva directa o
indirectamente del object tipo de clase.
La palabra clave object es simplemente un alias para la clase predefinida System.Object .
Tipo dynamic
El dynamic tipo, como object , puede hacer referencia a cualquier objeto. Cuando se aplican operadores a
expresiones de tipo dynamic , su resolución se aplaza hasta que se ejecuta el programa. Por lo tanto, si el
operador no se puede aplicar legalmente al objeto al que se hace referencia, no se proporciona ningún error
durante la compilación. En su lugar, se producirá una excepción cuando se produzca un error en la resolución
del operador en tiempo de ejecución.
Su finalidad es permitir el enlace dinámico, que se describe en detalle en el enlace dinámico.
dynamic se considera idéntico a object excepto en los siguientes aspectos:
Las operaciones en expresiones de tipo dynamic se pueden enlazar dinámicamente (enlace dinámico).
La inferencia de tipos (inferencia de tipos) preferirá dynamic object si ambas son candidatas.
public Box(T t) {
value = t;
}
}
La conversión boxing de un valor v de tipo T ahora consiste en ejecutar la expresión new Box<T>(v) y
devolver la instancia resultante como un valor de tipo object . Por lo tanto, las instrucciones
int i = 123;
object box = i;
int i = 123;
object box = new Box<int>(i);
Una clase Boxing como Box<T> la anterior no existe realmente y el tipo dinámico de un valor de conversión
boxing no es realmente un tipo de clase. En su lugar, un valor con conversión boxing de tipo T tiene el tipo
dinámico T y una comprobación de tipos dinámicos mediante el is operador puede simplemente hacer
referencia al tipo T . Por ejemplo,
int i = 123;
object box = i;
if (box is int) {
Console.Write("Box contains an int");
}
dará como resultado la cadena " Box contains an int " en la consola.
Una conversión boxing implica que se realice una copia del valor al que se va a aplicar la conversión boxing.
Esto es diferente de la conversión de un reference_type al tipo object , en el que el valor sigue haciendo
referencia a la misma instancia y simplemente se considera como el tipo menos derivado object . Por ejemplo,
dada la declaración
struct Point
{
public int x, y;
Para que una conversión unboxing a una non_nullable_value_type determinada se realice correctamente en
tiempo de ejecución, el valor del operando de origen debe ser una referencia a un valor de conversión boxing de
ese non_nullable_value_type. Si el operando de origen es null , System.NullReferenceException se produce una
excepción. Si el operando de origen es una referencia a un objeto incompatible, System.InvalidCastException se
produce una excepción.
Para que una conversión unboxing a una nullable_type determinada se realice correctamente en tiempo de
ejecución, el valor del operando de origen debe ser null o una referencia a un valor de conversión boxing del
non_nullable_value_type subyacente de la nullable_type. Si el operando de origen es una referencia a un objeto
incompatible, System.InvalidCastException se produce una excepción.
Tipos construidos
Una declaración de tipo genérico, por sí sola, denota un *tipo genérico sin enlazar , que se usa como
"Blueprint" para formar muchos tipos diferentes, por medio de la aplicación de argumentos de tipo. Los
argumentos de tipo se escriben entre corchetes angulares ( < y > ) inmediatamente después del nombre del
tipo genérico. Un tipo que incluye al menos un argumento de tipo se denomina tipo construido. Un tipo
construido se puede usar en la mayoría de los lugares del lenguaje en el que puede aparecer un nombre de tipo.
Un tipo genérico sin enlazar solo se puede usar dentro de un _typeof_expression * (el operador typeof).
Los tipos construidos también se pueden usar en expresiones como nombres simples (nombres simples) o al
obtener acceso a un miembro (acceso a miembros).
Cuando se evalúa un namespace_or_type_name , solo se tienen en cuenta los tipos genéricos con el número
correcto de parámetros de tipo. Por lo tanto, es posible usar el mismo identificador para identificar distintos
tipos, siempre que los tipos tengan distintos números de parámetros de tipo. Esto resulta útil cuando se
combinan clases genéricas y no genéricas en el mismo programa:
namespace Widgets
{
class Queue {...}
class Queue<TElement> {...}
}
namespace MyApplication
{
using Widgets;
class X
{
Queue q1; // Non-generic Widgets.Queue
Queue<int> q2; // Generic Widgets.Queue
}
}
Un type_name podría identificar un tipo construido aunque no especifique directamente los parámetros de tipo.
Esto puede ocurrir cuando un tipo está anidado dentro de una declaración de clase genérica y el tipo de
instancia de la declaración contenedora se usa implícitamente para la búsqueda de nombres (tipos anidados en
clases genéricas):
class Outer<T>
{
public class Inner {...}
En código no seguro, no se puede usar un tipo construido como unmanaged_type (tipos de puntero).
Argumentos de tipo
Cada argumento de una lista de argumentos de tipo es simplemente un tipo.
type_argument_list
: '<' type_arguments '>'
;
type_arguments
: type_argument (',' type_argument)*
;
type_argument
: type
;
En el código no seguro (código no seguro), un type_argument no puede ser un tipo de puntero. Cada
argumento de tipo debe satisfacer las restricciones en el parámetro de tipo correspondiente (restricciones de
parámetro de tipo).
Tipos abiertos y cerrados
Todos los tipos se pueden clasificar como * tipos abier tos _ o _ tipos cerrados *. Un tipo abierto es un tipo que
implica parámetros de tipo. Más concretamente:
Un parámetro de tipo define un tipo abierto.
Un tipo de matriz es un tipo abierto solo si su tipo de elemento es un tipo abierto.
Un tipo construido es un tipo abierto solo si uno o varios de sus argumentos de tipo es un tipo abierto. Un
tipo anidado construido es un tipo abierto si y solo si uno o varios de sus argumentos de tipo o los
argumentos de tipo de sus tipos contenedores es un tipo abierto.
Un tipo cerrado es un tipo que no es un tipo abierto.
En tiempo de ejecución, todo el código dentro de una declaración de tipos genéricos se ejecuta en el contexto de
un tipo construido cerrado que se creó mediante la aplicación de argumentos de tipo a la declaración genérica.
Cada parámetro de tipo dentro del tipo genérico se enlaza a un tipo en tiempo de ejecución determinado. El
procesamiento en tiempo de ejecución de todas las instrucciones y expresiones siempre se produce con tipos
cerrados y los tipos abiertos solo se producen durante el procesamiento en tiempo de compilación.
Cada tipo construido cerrado tiene su propio conjunto de variables estáticas, que no se comparten con otros
tipos construidos cerrados. Puesto que no existe un tipo abierto en tiempo de ejecución, no hay variables
estáticas asociadas a un tipo abierto. Dos tipos construidos cerrados son del mismo tipo si se construyen a
partir del mismo tipo genérico sin enlazar y sus argumentos de tipo correspondientes son del mismo tipo.
Tipos enlazados y sin enlazar
El término *tipo sin enlazar _ hace referencia a un tipo no genérico o a un tipo genérico sin enlazar. El término
_ Bound Type* hace referencia a un tipo no genérico o a un tipo construido.
Un tipo sin enlazar hace referencia a la entidad declarada por una declaración de tipos. Un tipo genérico sin
enlazar no es en sí mismo un tipo y no se puede usar como el tipo de una variable, argumento o valor devuelto,
o como un tipo base. La única construcción en la que se puede hacer referencia a un tipo genérico sin enlazar es
la typeof expresión (el operador typeof).
Satisfacer restricciones
Siempre que se hace referencia a un tipo construido o a un método genérico, los argumentos de tipo
proporcionados se comprueban con las restricciones de parámetro de tipo declaradas en el tipo o método
genérico (restricciones de parámetro de tipo). Para cada where cláusula, el argumento de tipo A que se
corresponde con el parámetro de tipo con nombre se compara con cada restricción de la manera siguiente:
Si la restricción es un tipo de clase, un tipo de interfaz o un parámetro de tipo, permita C representar esa
restricción con los argumentos de tipo proporcionados que se sustituyen por los parámetros de tipo que
aparecen en la restricción. Para satisfacer la restricción, debe ser el caso de que el tipo A se pueda convertir
al tipo C mediante una de las siguientes acciones:
Una conversión de identidad (conversión de identidad)
Una conversión de referencia implícita (conversiones de referencia implícita)
Una conversión boxing (conversiones boxing), siempre que el tipo a sea un tipo de valor que no
acepte valores NULL.
Referencia implícita, conversión boxing o conversión de parámetros de tipo de un parámetro de tipo
A a C .
Si la restricción es la restricción de tipo de referencia ( class ), el tipo A debe cumplir uno de los siguientes
elementos:
A es un tipo de interfaz, tipo de clase, tipo de delegado o tipo de matriz. Tenga en cuenta que
System.ValueType y System.Enum son tipos de referencia que satisfacen esta restricción.
A es un parámetro de tipo que se sabe que es un tipo de referencia (restricciones de parámetro de
tipo).
Si la restricción es la restricción de tipo de valor ( struct ), el tipo A debe cumplir uno de los siguientes
elementos:
A es un tipo de estructura o un tipo de enumeración, pero no un tipo que acepta valores NULL. Tenga
en cuenta que System.ValueType y System.Enum son tipos de referencia que no cumplen esta
restricción.
A es un parámetro de tipo que tiene la restricción de tipo de valor (restricciones de parámetro de
tipo).
Si la restricción es la restricción del constructor new() , el tipo A no debe ser abstract y debe tener un
constructor sin parámetros público. Esto se cumple si se cumple una de las siguientes condiciones:
A es un tipo de valor, ya que todos los tipos de valor tienen un constructor predeterminado público
(constructores predeterminados).
A es un parámetro de tipo que tiene la restricción de constructor (restricciones de parámetro de
tipo).
A es un parámetro de tipo que tiene la restricción de tipo de valor (restricciones de parámetro de
tipo).
A es una clase que no es abstract y contiene un constructor declarado explícitamente public sin
parámetros.
A no es abstract y tiene un constructor predeterminado (constructores predeterminados).
Se produce un error en tiempo de compilación si los argumentos de tipo especificados no satisfacen una o
varias restricciones de un parámetro de tipo.
Dado que los parámetros de tipo no se heredan, las restricciones nunca se heredan. En el ejemplo siguiente, D
debe especificar la restricción en su parámetro de tipo T para que T cumpla la restricción impuesta por la
clase base B<T> . En cambio, E la clase no necesita especificar una restricción, porque List<T> implementa
IEnumerable para any T .
Parámetros de tipo
Un parámetro de tipo es un identificador que designa un tipo de valor o un tipo de referencia al que está
enlazado el parámetro en tiempo de ejecución.
type_parameter
: identifier
;
Dado que se pueden crear instancias de un parámetro de tipo con muchos argumentos de tipo reales diferentes,
los parámetros de tipo tienen operaciones y restricciones ligeramente diferentes a las de otros tipos. Entre ellos,
se incluye:
Un parámetro de tipo no se puede usar directamente para declarar una clase base (clase base) o una interfaz
(listas de parámetros de tipo variante).
Las reglas para la búsqueda de miembros en parámetros de tipo dependen de las restricciones, si las hay,
que se aplican al parámetro de tipo. Se detallan en la búsqueda de miembros.
Las conversiones disponibles para un parámetro de tipo dependen de las restricciones, si las hay, que se
aplican al parámetro de tipo. Se detallan en conversiones implícitas que implican parámetros de tipo y
conversiones dinámicas explícitas.
El literal null no se puede convertir en un tipo proporcionado por un parámetro de tipo, excepto si se sabe
que el parámetro de tipo es un tipo de referencia (conversiones implícitas que implican parámetros de tipo).
Sin embargo, default en su lugar se puede utilizar una expresión (expresiones de valor predeterminado).
Además, un valor con un tipo proporcionado por un parámetro de tipo puede compararse con null
mediante == y != (operadores de igualdad de tipos dereferencia), a menos que el parámetro de tipo tenga
la restricción de tipo de valor.
Una new expresión (expresiones de creación de objetos) solo se puede utilizar con un parámetro de tipo si el
parámetro de tipo está restringido por un constructor_constraint o la restricción de tipo de valor
(restricciones de parámetro de tipo).
Un parámetro de tipo no se puede usar en ningún lugar dentro de un atributo.
No se puede usar un parámetro de tipo en un acceso de miembro (acceso a miembros) o nombre de tipo
(espacio de nombresy nombres de tipo) para identificar un miembro estático o un tipo anidado.
En código no seguro, no se puede usar un parámetro de tipo como unmanaged_type (tipos de puntero).
Como tipo, los parámetros de tipo son únicamente una construcción en tiempo de compilación. En tiempo de
ejecución, cada parámetro de tipo se enlaza a un tipo en tiempo de ejecución que se especificó proporcionando
un argumento de tipo a la declaración de tipos genéricos. Por lo tanto, el tipo de una variable declarada con un
parámetro de tipo, en tiempo de ejecución, será un tipo construido cerrado (tipos abiertos y cerrados). La
ejecución en tiempo de ejecución de todas las instrucciones y expresiones que impliquen parámetros de tipo
usa el tipo real que se proporcionó como argumento de tipo para ese parámetro.
Después de estas asignaciones, el delegado del hace referencia a un método que devuelve x + 1 y el árbol de
expresión exp hace referencia a una estructura de datos que describe la expresión x => x + 1 .
La definición exacta del tipo genérico, Expression<D> así como las reglas precisas para construir un árbol de
expresión cuando una expresión lambda se convierte en un tipo de árbol de expresión, está fuera del ámbito de
esta especificación.
Es importante hacer dos cosas:
No todas las expresiones lambda se pueden convertir en árboles de expresión. Por ejemplo, las
expresiones lambda con cuerpos de instrucciones y expresiones lambda que contienen expresiones de
asignación no se pueden representar. En estos casos, todavía existe una conversión, pero se producirá un
error en tiempo de compilación. Estas excepciones se detallan en conversiones de funciones anónimas.
Expression<D> proporciona un método Compile de instancia que genera un delegado de tipo D :
Al invocar este delegado se produce el código representado por el árbol de expresión que se va a
ejecutar. Por lo tanto, dado que las definiciones anteriores, del y DEL2 son equivalentes, y las dos
instrucciones siguientes tendrán el mismo efecto:
int i1 = del(1);
int i2 = del2(1);
Las variables representan ubicaciones de almacenamiento. Cada variable tiene un tipo que determina qué
valores se pueden almacenar en la variable. C# es un lenguaje con seguridad de tipos y el compilador de C#
garantiza que los valores almacenados en variables siempre sean del tipo adecuado. El valor de una variable se
puede cambiar mediante la asignación o mediante el uso de los ++ operadores -- y .
Se debe asignar definitivamente una variable (asignación definitiva)para poder obtener su valor.
Como se describe en las secciones siguientes, las variables se asignan inicialmente a * o _*inicialmente sin
asignar **. Una variable asignada inicialmente tiene un valor inicial bien definido y siempre se considera
asignada definitivamente. Una variable inicialmente sin asignación no tiene ningún valor inicial. Para que una
variable inicialmente sin asignar se considere definitivamente asignada en una ubicación determinada, debe
producirse una asignación a la variable en todas las rutas de ejecución posibles que conducen a esa ubicación.
Categorías de variable
C# define siete categorías de variables: variables estáticas, variables de instancia, elementos de matriz,
parámetros de valor, parámetros de referencia, parámetros de salida y variables locales. Las secciones siguientes
describen cada una de estas categorías.
En el ejemplo
class A
{
public static int x;
int y;
x es una variable estática, es una variable de instancia, es un elemento de matriz, es un parámetro de valor, es
un parámetro de referencia, es un parámetro de salida y y v[0] es una variable a b c i local.
Variables estáticas
Un campo declarado con el static modificador se denomina variable estática . Una variable estática entra en
vigor antes de la ejecución del constructor estático(constructores estáticos) para su tipo de contenido y deja de
existir cuando el dominio de aplicación asociado deja de existir.
El valor inicial de una variable estática es el valor predeterminado(valores predeterminados)del tipo de la
variable.
A efectos de la comprobación de asignación definitiva, se considera que una variable estática está asignada
inicialmente.
Variables de instancia
Un campo declarado sin el static modificador se denomina variable de instancia .
Variables de instancia en clases
Una variable de instancia de una clase entra en vigor cuando se crea una nueva instancia de esa clase y deja de
existir cuando no hay referencias a esa instancia y se ha ejecutado el destructor de la instancia (si existe).
El valor inicial de una variable de instancia de una clase es el valor predeterminado (valores predeterminados)
del tipo de la variable.
Para la comprobación de asignación definitiva, una variable de instancia de una clase se considera asignada
inicialmente.
Variables de instancia en estructuras
Una variable de instancia de un struct tiene exactamente la misma duración que la variable de estructura a la
que pertenece. En otras palabras, cuando una variable de un tipo de estructura entra en existencia o deja de
existir, también lo hacen las variables de instancia de la estructura .
El estado de asignación inicial de una variable de instancia de un struct es el mismo que el de la variable de
estructura contenedora. En otras palabras, cuando una variable de estructura se considera asignada
inicialmente, también lo son sus variables de instancia y, cuando una variable de estructura se considera
inicialmente sin asignar, sus variables de instancia tampoco tienen asignación.
Elementos de matriz
Los elementos de una matriz se crean cuando se crea una instancia de matriz y dejan de existir cuando no hay
referencias a esa instancia de matriz.
El valor inicial de cada uno de los elementos de una matriz es el valor predeterminado (Valores
predeterminados) del tipo de los elementos de la matriz.
Para la comprobación de asignación definitiva, un elemento de matriz se considera asignado inicialmente.
Parámetros de valor
Un parámetro declarado sin un ref modificador o es un parámetro de out valor .
Un parámetro de valor entra en vigor tras la invocación del miembro de función (método, constructor de
instancia, accessor u operador) o de la función anónima a la que pertenece el parámetro, y se inicializa con el
valor del argumento proporcionado en la invocación. Normalmente, un parámetro de valor deja de existir al
devolver el miembro de función o la función anónima. Sin embargo, si una función anónima captura el
parámetro value (expresiones de funciónanónimas),su duración se extiende al menos hasta que el delegado o el
árbol de expresión creado a partir de esa función anónima sea apto para la recolección de elementos no
utilizados.
Para la comprobación de asignación definitiva, un parámetro de valor se considera asignado inicialmente.
Parámetros de referencia
Un parámetro declarado con un ref modificador es un parámetro de referencia .
Un parámetro de referencia no crea una nueva ubicación de almacenamiento. En su lugar, un parámetro de
referencia representa la misma ubicación de almacenamiento que la variable especificada como argumento en
el miembro de función o la invocación de función anónima. Por lo tanto, el valor de un parámetro de referencia
siempre es el mismo que la variable subyacente.
Las siguientes reglas de asignación definidas se aplican a los parámetros de referencia. Tenga en cuenta las
distintas reglas para los parámetros de salida que se describen en Parámetros de salida.
Una variable debe asignarse definitivamente(definiciónde asignación ) antes de que se pueda pasar como
parámetro de referencia en una invocación de delegado o miembro de función.
Dentro de un miembro de función o una función anónima, un parámetro de referencia se considera asignado
inicialmente.
Dentro de un método de instancia o un accessor de instancia de un tipo struct, la palabra clave se comporta
exactamente como un parámetro de referencia del tipo this de estructura ( Esteacceso).
Parámetros de salida
Un parámetro declarado con un out modificador es un parámetro de salida .
Un parámetro de salida no crea una nueva ubicación de almacenamiento. En su lugar, un parámetro de salida
representa la misma ubicación de almacenamiento que la variable especificada como argumento en la
invocación de delegado o miembro de función. Por lo tanto, el valor de un parámetro de salida siempre es el
mismo que la variable subyacente.
Las siguientes reglas de asignación definidas se aplican a los parámetros de salida. Tenga en cuenta las distintas
reglas para los parámetros de referencia que se describen en Parámetros de referencia.
No es necesario asignar definitivamente una variable para poder pasarla como parámetro de salida en una
invocación de delegado o miembro de función.
Después de la finalización normal de una invocación de delegado o miembro de función, cada variable que
se pasó como parámetro de salida se considera asignada en esa ruta de acceso de ejecución.
Dentro de un miembro de función o una función anónima, un parámetro de salida se considera inicialmente
sin signo.
Todos los parámetros de salida de un miembro de función o una función anónima deben asignarse
definitivamente (Definite assignment) antes de que el miembro de función o la función anónima vuelvan con
normalidad.
Dentro de un constructor de instancia de un tipo de estructura, la palabra clave se comporta exactamente como
un parámetro de salida del tipo this de estructura ( Esteacceso).
Variables locales
Una variable local _ se declara mediante un _local_variable_declaration, que puede producirse en un bloque , un
for_statement , un switch_statement o un using_statement; o por un foreach_statement o un
specific_catch_clause para un try_statement.
La duración de una variable local es la parte de la ejecución del programa durante la cual se garantiza que el
almacenamiento se reservará para ella. Esta duración se extiende al menos desde la entrada en el bloque ,
for_statement, switch_statement, using_statement, foreach_statement o specific_catch_clause con el que está
asociada, hasta que la ejecución de ese bloque , for_statement, switch_statement, using_statement,
foreach_statement o specific_catch_clause finaliza de cualquier manera. (Al escribir un bloque delimitado o
llamar a un método se suspende, pero no finaliza, la ejecución del bloque actual , for_statement,
switch_statement, using_statement, foreach_statement o specific_catch_clause). Si una función anónima captura
la variable local (variablesexternas capturadas), su duración se extiende al menos hasta que el delegado o el
árbol de expresión creados a partir de la función anónima, junto con cualquier otro objeto que llegue a hacer
referencia a la variable capturada, sean aptos para la recolección de elementos no utilizados.
Si el bloque primario , for_statement, switch_statement, using_statement, foreach_statement o
specific_catch_clause se introduce de forma recursiva, se crea una nueva instancia de la variable local cada vez y
su local_variable_initializer, si existe, se evalúa cada vez.
Una variable local introducida por un local_variable_declaration no se inicializa automáticamente y, por tanto, no
tiene ningún valor predeterminado. Para la comprobación de asignación definitiva, una variable local
introducida por un local_variable_declaration se considera inicialmente sin asignación. Un
local_variable_declaration puede incluir un local_variable_initializer , en cuyo caso la variable se considera
definitivamente asignada solo después de la expresión de inicialización ( Instrucciones de declaración).
Dentro del ámbito de una variable local introducida por un local_variable_declaration, es un error en tiempo de
compilación hacer referencia a esa variable local en una posición textual que precede a su
local_variable_declarator. Si la declaración de variable local es implícita (declaraciones de variable local),
también es un error hacer referencia a la variable dentro de su local_variable_declarator.
Una variable local introducida por un foreach_statement o un specific_catch_clause se considera definitivamente
asignada en todo su ámbito.
La duración real de una variable local depende de la implementación. Por ejemplo, un compilador podría
determinar estáticamente que una variable local de un bloque solo se usa para una pequeña parte de ese
bloque. Con este análisis, el compilador podría generar código que da como resultado que el almacenamiento
de la variable tenga una duración más corta que su bloque que lo contiene.
El almacenamiento al que hace referencia una variable de referencia local se reclama independientemente de la
duración de esa variable de referencia local (Administración automática de memoria).
Valores predeterminados
Las siguientes categorías de variables se inicializan automáticamente en sus valores predeterminados:
Variables estáticas.
Variables de instancia de instancias de clase.
Elementos de matriz.
El valor predeterminado de una variable depende del tipo de la variable y se determina de la siguiente manera:
Para una variable de un value_type, el valor predeterminado es el mismo que el valor calculado por el
constructor predeterminado de value_type (Constructores predeterminados).
Para una variable de un reference_type, el valor predeterminado es null .
Asignación definitiva
En una ubicación determinada del código ejecutable de un miembro de función, se dice que una variable se
asigna definitivamente si el compilador puede demostrar, mediante un análisis de flujo estático
determinado(reglasprecisas para determinar la asignación definitiva), que la variable se ha inicializado
automáticamente o ha sido el destino de al menos una asignación. De forma informal, las reglas de asignación
definitiva son:
Una variable asignada inicialmente (variablesasignadas inicialmente)siempre se considera asignada
definitivamente.
Unavariableinicialmente sin asignar ( variables inicialmente sin asignar ) se considera asignada
definitivamente en una ubicación determinada si todas las rutas de ejecución posibles que conducen a esa
ubicación contienen al menos una de las siguientes:
Una asignación simple(asignación simple)en la que la variable es el operando izquierdo.
Expresión de invocación(expresionesde invocación) o expresión de creación de objetos(expresionesde
creación de objetos) que pasa la variable como parámetro de salida.
Para una variable local, una declaración de variable local(declaraciones de variable local)que incluye
un inicializador de variable.
La especificación formal subyacente a las reglas informales anteriores se describe en Variables
asignadasinicialmente, Variables inicialmente sin asignar y Reglas precisas para determinar la asignación
definitiva.
Los estados de asignación definidos de las variables de instancia de una variable struct_type se realiza un
seguimiento individual y colectivamente. Además de las reglas anteriores, las siguientes reglas se aplican a
struct_type variables y sus variables de instancia:
Una variable de instancia se considera definitivamente asignada si su variable struct_type se considera
asignada definitivamente.
Una struct_type variable se considera definitivamente asignada si cada una de sus variables de instancia se
considera asignada definitivamente.
La asignación definitiva es un requisito en los contextos siguientes:
Una variable debe asignarse definitivamente en cada ubicación donde se obtiene su valor. Esto garantiza que
los valores no definidos nunca se produzcan. La aparición de una variable en una expresión se considera
para obtener el valor de la variable, excepto cuando
la variable es el operando izquierdo de una asignación simple,
la variable se pasa como un parámetro de salida o
la variable es struct_type variable y se produce como el operando izquierdo de un acceso de miembro.
Una variable debe asignarse definitivamente en cada ubicación donde se pasa como parámetro de
referencia. Esto garantiza que el miembro de función que se invoca puede tener en cuenta el parámetro de
referencia asignado inicialmente.
Todos los parámetros de salida de un miembro de función deben asignarse definitivamente en cada
ubicación en la que el miembro de la función devuelve (a través de una instrucción o a través de la ejecución
que llega al final del cuerpo del return miembro de la función). Esto garantiza que los miembros de la
función no devuelvan valores indefinidos en los parámetros de salida, lo que permite al compilador
considerar una invocación de miembro de función que toma una variable como parámetro de salida
equivalente a una asignación a la variable.
La this variable de un constructor struct_type instancia de debe asignarse definitivamente en cada
ubicación donde devuelve ese constructor de instancia.
Variables asignadas inicialmente
Las siguientes categorías de variables se clasifican como asignadas inicialmente:
Variables estáticas.
Variables de instancia de instancias de clase.
Variables de instancia de variables de estructura asignadas inicialmente.
Elementos de matriz.
Parámetros de valor.
Parámetros de referencia.
Variables declaradas en una catch cláusula o una instrucción foreach .
Variables inicialmente sin signo
Las siguientes categorías de variables se clasifican como inicialmente sin signo:
Variables de instancia de variables de estructura inicialmente sin signo.
Parámetros de salida, incluida this la variable de constructores de instancia de struct.
Variables locales, excepto las declaradas en catch una cláusula o una instrucción foreach .
Reglas precisas para determinar la asignación definitiva
Para determinar que cada variable usada está asignada definitivamente, el compilador debe usar un proceso
equivalente al descrito en esta sección.
El compilador procesa el cuerpo de cada miembro de función que tiene una o varias variables inicialmente sin
asignación. Para cada variable inicialmente sin asignación v, el compilador determina un estado de asignación
definitiva _ para _v en cada uno de los puntos siguientes del miembro de función:
Al principio de cada instrucción
En el punto final(puntos de conexión y capacidad dealcance) de cada instrucción
En cada arco que transfiere el control a otra instrucción o al punto final de una instrucción
Al principio de cada expresión
Al final de cada expresión
El estado de asignación definido de v puede ser:
Asignado definitivamente. Esto indica que en todos los flujos de control posibles hasta este punto, se ha
asignado un valor a v.
No está asignado definitivamente. Para el estado de una variable al final de una expresión de tipo , el estado
de una variable que no está asignada definitivamente puede (pero no necesariamente) entrar en uno de los
siguientes bool subes estados:
Se ha asignado definitivamente después de la expresión true. Este estado indica que v se asigna
definitivamente si la expresión booleana se evaluó como true, pero no se asigna necesariamente si la
expresión booleana se evaluó como false.
Se ha asignado definitivamente después de la expresión false. Este estado indica que v se asigna
definitivamente si la expresión booleana se evaluó como false, pero no se asigna necesariamente si la
expresión booleana se evaluó como true.
Las reglas siguientes rigen cómo se determina el estado de una variable v en cada ubicación.
Reglas generales para instrucciones
v no se asigna definitivamente al principio de un cuerpo de miembro de función.
v se asigna definitivamente al principio de cualquier instrucción inaccesible.
El estado de asignación definido de v al principio de cualquier otra instrucción se determina comprobando el
estado de asignación definido de v en todas las transferencias de flujo de control que tienen como destino el
principio de esa instrucción. Si (y solo si) v se asigna definitivamente en todas estas transferencias de flujo de
control, v se asigna definitivamente al principio de la instrucción . El conjunto de posibles transferencias de
flujo de control se determina de la misma manera que para comprobar la capacidad de alcance de la
instrucción (puntosde conexión y capacidad de alcance).
El estado de asignación definitiva de v en el punto final de un bloque, , , , , , , o instrucción se determina
comprobando el estado de asignación definitiva de v en todas las transferencias de flujo de control que
tienen como destino el punto final de checked esa unchecked if while do for foreach lock using
switch instrucción. Si v se asigna definitivamente en todas estas transferencias de flujo de control, v se
asigna definitivamente al punto final de la instrucción. De lo contrario; v no se asigna definitivamente en el
punto final de la instrucción . El conjunto de posibles transferencias de flujo de control se determina de la
misma manera que para comprobar la capacidad de alcance de la instrucción (puntosde conexión y
capacidad de alcance).
Instrucciones block, checked y unchecked
El estado de asignación definitiva de v en la transferencia de control a la primera instrucción de la lista de
instrucciones del bloque (o hasta el punto final del bloque, si la lista de instrucciones está vacía) es el mismo que
la instrucción de asignación definitiva de v antes de la instrucción block, checked o unchecked .
Instrucciones de expresión
Para una instrucción de expresión stmt que consta de la expresión expr:
v tiene el mismo estado de asignación definitiva al principio de expr que al principio de stmt.
Si v si se asigna definitivamente al final de expr, se asigna definitivamente al punto final de stmt; de lo
contrario; no se asigna definitivamente en el punto final de stmt.
Instrucciones de declaración
Si stmt es una instrucción de declaración sin inicializadores, v tiene el mismo estado de asignación definitiva
en el punto final de stmt que al principio de stmt.
Si stmt es una instrucción de declaración con inicializadores, el estado de asignación definido para v se
determina como si stmt fuera una lista de instrucciones, con una instrucción de asignación para cada
declaración con un inicializador (en el orden de declaración).
Instrucciones If
Para un if stmt de instrucción con el formato:
v tiene el mismo estado de asignación definido al principio de expr que al principio de stmt.
Si v se asigna definitivamente al final de expr, se asigna definitivamente en la transferencia del flujo de
control a then_stmt y a else_stmt o al punto final de stmt si no hay ninguna cláusula else.
Si v tiene el estado "asignado definitivamente después de la expresión verdadera" al final de expr, se asigna
definitivamente en la transferencia de flujo de control a then_stmt y no se asigna definitivamente en la
transferencia de flujo de control a else_stmt o al punto final de stmt si no hay ninguna cláusula else.
Si v tiene el estado "asignado definitivamente después de la expresión falsa" al final de expr, se asigna
definitivamente en la transferencia de flujo de control a else_stmt y no se asigna definitivamente en la
transferencia de flujo de control a then_stmt. Se asigna definitivamente al punto final de stmt si y solo si se
asigna definitivamente en el punto final de then_stmt.
De lo contrario, se considera que v no está asignado definitivamente en la transferencia del flujo de control a
then_stmt o else_stmt o al punto final de stmt si no hay ninguna cláusula else.
Instrucciones switch
En una switch instrucción stmt con una expresión de control expr:
El estado de asignación definido de v al principio de expr es el mismo que el estado de v al principio de stmt.
El estado de asignación definido de v en la transferencia de flujo de control a una lista de instrucciones de
bloque de modificador accesible es el mismo que el estado de asignación definido de v al final de expr.
Instrucciones While
Para una while instrucción stmt del formulario:
v tiene el mismo estado de asignación definido al principio de expr que al principio de stmt.
Si v se asigna definitivamente al final de expr, se asigna definitivamente en la transferencia del flujo de
control a while_body y al punto final de stmt.
Si v tiene el estado "definitivamente asignado después de la expresión true" al final de expr, se asigna
definitivamente en la transferencia del flujo de control a while_body, pero no se asigna definitivamente al
punto final de stmt.
Si v tiene el estado "definitivamente asignado después de la expresión falsa" al final de expr, se asigna
definitivamente en la transferencia del flujo de control al punto final de stmt, pero no se asigna
definitivamente en la transferencia del flujo de control a while_body.
Instrucciones Do
Para un do stmt de instrucción con el formato :
v tiene el mismo estado de asignación definido en la transferencia del flujo de control desde el principio de
stmt a do_body que al principio de stmt.
v tiene el mismo estado de asignación definido al principio de expr que al final de do_body.
Si v se asigna definitivamente al final de expr, se asigna definitivamente en la transferencia del flujo de
control al punto final de stmt.
Si v tiene el estado "definitivamente asignado después de la expresión false" al final de expr, se asigna
definitivamente en la transferencia del flujo de control al punto final de stmt.
Para instrucciones
Definición de la comprobación de for asignación para una instrucción del formulario:
{
for_initializer ;
while ( for_condition ) {
embedded_statement ;
for_iterator ;
}
}
throw expr ;
El estado de asignación definido de v al principio de expr es el mismo que el estado de asignación definido de v
al principio de stmt.
Instrucciones Return
Para una instrucción stmt del formulario
return expr ;
El estado de asignación definido de v al principio de expr es el mismo que el estado de asignación definido
de v al principio de stmt.
Si v es un parámetro de salida, debe asignarse definitivamente:
after expr
o al final del finally bloque de o try - finally try - catch - finally que incluye la return
instrucción .
Para una instrucción stmt del formulario:
return ;
try try_block
catch(...) catch_block_1
...
catch(...) catch_block_n
try try_block
catch(...) catch_block_1
...
catch(...) catch_block_n
finally *finally_block*
se hace como si la instrucción fuera try - finally una instrucción que incluye una try - catch instrucción :
try {
try try_block
catch(...) catch_block_1
...
catch(...) catch_block_n
}
finally finally_block
En el ejemplo siguiente se muestra cómo los distintos bloques de una instrucción ( la instrucción try ) afectan a
try la asignacióndefinitiva.
class A
{
static void F() {
int i, j;
try {
goto LABEL;
// neither i nor j definitely assigned
i = 1;
// i definitely assigned
}
catch {
// neither i nor j definitely assigned
i = 3;
// i definitely assigned
}
finally {
// neither i nor j definitely assigned
j = 5;
// j definitely assigned
}
// i and j definitely assigned
LABEL:;
// j definitely assigned
}
}
Instrucciones Foreach
Para un foreach stmt de instrucción con el formato :
El estado de asignación definido de v al principio de expr es el mismo que el estado de v al principio de stmt.
El estado de asignación definido de v en la transferencia de flujo de control a embedded_statement es el
mismo que el estado de v al final de expr.
Instrucciones Yield
Para una yield return instrucción stmt del formulario:
El estado de asignación definido de v al principio de expr es el mismo que el estado de v al principio de stmt.
El estado de asignación definido de v al final de stmt es el mismo que el estado de v al final de expr.
Una yield break instrucción no tiene ningún efecto en el estado de asignación definido.
Reglas generales para expresiones simples
La regla siguiente se aplica a estos tipos de expresiones: literales(literales),nombres simples(nombressimples),
expresiones de acceso a miembros(accesoa miembros), expresiones de acceso base no indexadas(accesobase),
expresiones (el operador typeof), expresiones de valor predeterminado (expresiones de valor predeterminado) y
typeof nameof expresiones(expresiones Nameof).
El estado de asignación definido de v al final de dicha expresión es el mismo que el estado de asignación
definido de v al principio de la expresión.
Reglas generales para expresiones con expresiones insertadas
Las reglas siguientes se aplican a estos tipos de expresiones: expresiones entre paréntesis(expresionesentre
paréntesis), expresiones de acceso a elementos(accesoa elementos),expresiones de acceso base con indexación
(accesobase),expresiones de incremento y decremento(operadoresde incremento y decremento de postfijo,
operadores de incremento y decremento de prefijo), expresiones de conversión(expresionesde conversión),
expresiones + unarias, - , ~ , * expresiones, binary , , , , , , expressions (operadores aritméticos , operadores
Shift , operadores relacionales y de prueba de tipos , operadores lógicos), expresiones de asignación compuesta
(asignación compuesta) y + - * / % << >> < <= > >= == != is as & | ^ checked
expresiones (operadores comprobados unchecked y no comprobados), además de expresiones de creación de
matrices y delegados (el nuevo operador ).
Cada una de estas expresiones tiene una o varias sub expressions que se evalúan incondicionalmente en un
orden fijo. Por ejemplo, el operador binario evalúa el lado izquierdo del operador y, a % continuación, el lado
derecho. Una operación de indexación evalúa la expresión indizada y, a continuación, evalúa cada una de las
expresiones de índice, en orden de izquierda a derecha. Para una expresión expr, que tiene sub expressions e1,
e2, ..., eN, eN evaluadas en ese orden:
El estado de asignación definitiva de v al principio de e1 es el mismo que el estado de asignación definitiva al
principio de expr.
El estado de asignación definitiva de v al principio de ei (i mayor que uno) es el mismo que el estado de
asignación definitiva al final de la subexpresión anterior.
El estado de asignación definitiva de v al final de expr es el mismo que el estado de asignación definitiva al
final de eN.
Expresiones de invocación y expresiones de creación de objetos
Para una expresión de invocación expr del formulario:
class A
{
static void F(int x, int y) {
int i;
if (x >= 0 && (i = y) >= 0) {
// i definitely assigned
}
else {
// i not definitely assigned
}
// i not definitely assigned
}
}
class A
{
static void G(int x, int y) {
int i;
if (x >= 0 || (i = y) >= 0) {
// i not definitely assigned
}
else {
// i definitely assigned
}
// i not definitely assigned
}
}
void F() {
int max;
max = 5;
DoWork(f);
}
genera un error en tiempo de compilación, ya max que no se asigna definitivamente donde se declara la
función anónima. En el ejemplo
delegate void D();
void F() {
int n;
D d = () => { n = 1; };
d();
también genera un error en tiempo de compilación, ya que la asignación a en la función anónima no afecta al
estado de asignación definido de fuera de n n la función anónima.
Referencias de variables
Un variable_reference es una expresión que se clasifica como una variable. Un variable_reference indica una
ubicación de almacenamiento a la que se puede acceder tanto para capturar el valor actual como para
almacenar un nuevo valor.
variable_reference
: expression
;
Una *conversión _ permite tratar una expresión como si se tratase de un tipo determinado. Una conversión
puede hacer que una expresión de un tipo determinado se trate como si tuviera un tipo diferente o que una
expresión sin un tipo obtenga un tipo. Las conversiones pueden ser implícitas o _ explícitas *, y esto determina si
se requiere una conversión explícita. Por ejemplo, la conversión del tipo int al tipo long es implícita, por lo
que las expresiones de tipo se int pueden tratar implícitamente como de tipo long . La conversión opuesta,
del tipo long al tipo int , es explícita y, por tanto, se requiere una conversión explícita.
int a = 123;
long b = a; // implicit conversion from int to long
int c = (int) b; // explicit conversion from long to int
Algunas conversiones están definidas por el lenguaje. Los programas también pueden definir sus propias
conversiones (conversiones definidas por el usuario).
Conversiones implícitas
Las conversiones siguientes se clasifican como conversiones implícitas:
Conversiones de identidad
Conversiones numéricas implícitas
Conversiones de enumeración implícitas
Conversiones de cadenas interpoladas IMPLÍCITAS
Conversiones implícitas que aceptan valores NULL
Conversiones de literales null
Conversiones de referencias implícitas
Conversiones Boxing
Conversiones dinámicas IMPLÍCITAS
Conversiones implícitas de expresiones constantes
Conversiones implícitas definidas por el usuario
Conversiones de función anónima
Conversiones de grupos de métodos
Las conversiones implícitas pueden producirse en diversas situaciones, incluidas las invocaciones de miembros
de función (comprobación en tiempo de compilación de la resolución dinámica de sobrecarga), las expresiones
de conversión (expresiones de conversión) y las asignaciones (operadores deasignación).
Las conversiones implícitas predefinidas siempre se realizan correctamente y nunca provocan que se inicien
excepciones. Las conversiones implícitas definidas por el usuario diseñadas correctamente deberían presentar
también estas características.
En lo que respecta a la conversión, los tipos object y dynamic se consideran equivalentes.
Sin embargo, las conversiones dinámicas (conversiones dinámicasimplícitas y conversiones dinámicas
explícitas) solo se aplican a las expresiones de tipo dynamic (el tipo dinámico).
Conversión de identidad
Una conversión de identidad convierte de cualquier tipo al mismo tipo. Esta conversión existe de manera que se
puede decir que una entidad que ya tiene un tipo necesario se puede convertir en ese tipo.
Dado object dynamic que y se consideran equivalentes, hay una conversión de identidad entre object y
dynamic , y entre los tipos construidos que son iguales al reemplazar todas las apariciones de dynamic por
object .
Conversiones numéricas implícitas
Las conversiones numéricas implícitas son:
De sbyte a short , int ,,, long float double o decimal .
De byte a short , ushort , int , uint , long , ulong , float , double o decimal .
De short a int , long , float , double o decimal .
De ushort a int , uint , long , ulong , float , double o decimal .
De int a long , float , double o decimal .
De uint a long , ulong , float , double o decimal .
De long a float , double o decimal .
De ulong a float , double o decimal .
De char a ushort , int , uint , long , ulong , float , double o decimal .
De float a double .
Las conversiones de int , uint , long o ulong a float y desde long o ulong a double pueden provocar
una pérdida de precisión, pero nunca producirán una pérdida de magnitud. El resto de conversiones numéricas
implícitas nunca pierden información.
No hay conversiones implícitas al char tipo, por lo que los valores de los otros tipos enteros no se convierten
automáticamente al char tipo.
Conversiones de enumeración implícitas
Una conversión de enumeración implícita permite convertir el decimal_integer_literal 0 en cualquier
enum_type y en cualquier nullable_type cuyo tipo subyacente sea un enum_type. En el último caso, la
conversión se evalúa convirtiendo en el enum_type subyacente y ajustando el resultado (tipos que aceptan
valores NULL).
Conversiones de cadenas interpoladas IMPLÍCITAS
Una conversión de cadena interpolada implícita permite convertir un interpolated_string_expression (cadenas
interpoladas) en System.IFormattable o System.FormattableString (que implementa System.IFormattable ).
Cuando se aplica esta conversión, un valor de cadena no se compone de la cadena interpolada. En su lugar
System.FormattableString , se crea una instancia de, tal y como se describe en cadenas interpoladas.
object o = "object"
dynamic d = "dynamic";
Las asignaciones a s2 y i ambos emplean conversiones dinámicas IMPLÍCITAS, donde el enlace de las
operaciones se suspende hasta el tiempo de ejecución. En tiempo de ejecución, las conversiones implícitas se
buscan desde el tipo en tiempo de ejecución de, d -- string hasta el tipo de destino. Una conversión se
encuentra en string pero no en int .
Conversiones implícitas de expresiones constantes
Una conversión de expresión constante implícita permite las siguientes conversiones:
Un constant_expression (expresiones constantes) de tipo int se puede convertir al tipo sbyte , byte ,
short , ushort , uint o ulong , siempre que el valor de la constant_expression esté dentro del intervalo
del tipo de destino.
Un constant_expression de tipo long se puede convertir al tipo ulong , siempre que el valor de la
constant_expression no sea negativo.
Conversiones implícitas que implican parámetros de tipo
Existen las siguientes conversiones implícitas para un parámetro de tipo determinado T :
De T a su clase base efectiva C , de T a cualquier clase base de C y de T a cualquier interfaz
implementada por C . En tiempo de ejecución, si T es un tipo de valor, la conversión se ejecuta como
conversión boxing. De lo contrario, la conversión se ejecuta como una conversión de referencia implícita o
una conversión de identidad.
De T a un tipo de interfaz I en un T conjunto de interfaz eficaz y de T a cualquier interfaz base de I .
En tiempo de ejecución, si T es un tipo de valor, la conversión se ejecuta como conversión boxing. De lo
contrario, la conversión se ejecuta como una conversión de referencia implícita o una conversión de
identidad.
De T a un parámetro de tipo U , proporcionado T depende de U (restricciones de parámetro detipo). En
tiempo de ejecución, si U es un tipo de valor, T y U son necesariamente el mismo tipo y no se realiza
ninguna conversión. De lo contrario, si T es un tipo de valor, la conversión se ejecuta como una conversión
boxing. De lo contrario, la conversión se ejecuta como una conversión de referencia implícita o una
conversión de identidad.
Desde el literal null hasta T , T se sabe que se trata de un tipo de referencia.
De T a un tipo de referencia I si tiene una conversión implícita a un tipo de referencia S0 y S0 tiene una
conversión de identidad en S . En tiempo de ejecución, la conversión se ejecuta de la misma forma que la
conversión a S0 .
De T a un tipo de interfaz I si tiene una conversión implícita a un tipo de interfaz o delegado I0 y I0 se
pueden convertir en varianza I (conversión devarianza). En tiempo de ejecución, si T es un tipo de valor, la
conversión se ejecuta como conversión boxing. De lo contrario, la conversión se ejecuta como una
conversión de referencia implícita o una conversión de identidad.
Si T se sabe que es un tipo de referencia (restricciones de parámetro de tipo), las conversiones anteriores se
clasifican como conversiones de referencia IMPLÍCITAS (conversiones de referencia implícita). Si T no se sabe
que es un tipo de referencia, las conversiones anteriores se clasifican como conversiones Boxing
(conversionesBoxing).
Conversiones implícitas definidas por el usuario
Una conversión implícita definida por el usuario se compone de una conversión implícita estándar opcional,
seguida de una ejecución de un operador de conversión implícita definido por el usuario, seguida de otra
conversión implícita estándar opcional. Las reglas exactas para evaluar las conversiones implícitas definidas por
el usuario se describen en procesamiento de conversiones implícitas definidas por el usuario.
Conversiones de funciones anónimas y conversiones de grupos de métodos
Las funciones anónimas y los grupos de métodos no tienen tipos en y, pero se pueden convertir implícitamente
en tipos de delegado o tipos de árbol de expresión. Las conversiones de función anónima se describen con más
detalle en conversiones de funciones anónimas y conversiones de grupos de métodos en conversiones de
grupos de métodos.
Conversiones explícitas
Las conversiones siguientes se clasifican como conversiones explícitas:
Todas las conversiones implícitas.
Conversiones numéricas explícitas.
Conversiones de enumeración explícitas.
Conversiones explícitas que aceptan valores NULL.
Conversiones de referencia explícitas.
Conversiones explícitas de la interfaz.
Conversiones unboxing.
Conversiones dinámicas explícitas
Conversiones explícitas definidas por el usuario.
Las conversiones explícitas pueden producirse en expresiones de conversión (expresiones de conversión).
El conjunto de conversiones explícitas incluye todas las conversiones implícitas. Esto significa que se permiten
expresiones de conversión redundantes.
Las conversiones explícitas que no son conversiones implícitas son conversiones que no se pueden demostrar
que siempre se realizan correctamente, conversiones en las que se sabe que podrían perder información y
conversiones entre dominios de tipos lo suficientemente diferentes como para merecen la notación explícita.
Conversiones numéricas explícitas
Las conversiones numéricas explícitas son las conversiones de un numeric_type a otro numeric_type para el que
no existe una conversión numérica implícita (Conversiones numéricas implícitas):
De sbyte a byte , ushort , uint , ulong o char .
Desde byte hasta sbyte y char .
De short a sbyte , byte ,,, ushort uint ulong o char .
De ushort a sbyte , byte , short o char .
De int a sbyte , byte , short , ushort , uint , ulong o char .
De uint a sbyte , byte ,,, short ushort int o char .
De long a sbyte , byte , short , ushort , int , uint , ulong o char .
De ulong a sbyte , byte , short , ushort , int , uint , long o char .
De char a sbyte , byte o short .
De float a sbyte , byte , short , ushort , int , uint , long , ulong , char o decimal .
De double a sbyte , byte , short , ushort , int , uint , long , ulong , char , float o decimal .
De decimal a sbyte , byte , short , ushort , int , uint , long , ulong , char , float o double .
Dado que las conversiones explícitas incluyen todas las conversiones numéricas implícitas y explícitas, siempre
es posible convertir de cualquier numeric_type a cualquier otra numeric_type mediante una expresión de
conversión (expresiones de conversión).
Las conversiones numéricas explícitas podrían perder información o provocar que se produzcan excepciones.
Una conversión numérica explícita se procesa de la siguiente manera:
En el caso de una conversión de un tipo entero a otro tipo entero, el procesamiento depende del contexto de
comprobación de desbordamiento (los operadores comprobados y sin comprobar) en el que tiene lugar la
conversión:
En un checked contexto, la conversión se realiza correctamente si el valor del operando de origen está
dentro del intervalo del tipo de destino, pero produce una excepción System.OverflowException si el
valor del operando de origen está fuera del intervalo del tipo de destino.
En un unchecked contexto, la conversión siempre se realiza correctamente y continúa como se indica
a continuación.
Si el tipo de origen es mayor que el tipo de destino, el valor de origen se trunca al descartar sus
bits "extra" más significativos. El resultado se trata como un valor del tipo de destino.
Si el tipo de origen es menor que el tipo de destino, el valor de origen se amplía mediante
signos o ceros para que tenga el mismo tamaño que el tipo de destino. La ampliación mediante
signos se usa si el tipo de origen tiene signo; se emplea ampliación mediante ceros si el tipo de
origen no tiene signo. El resultado se trata como un valor del tipo de destino.
Si el tipo de origen es del mismo tamaño que el tipo de destino, el valor de origen se trata
como un valor del tipo de destino.
En el caso de una conversión de decimal a un tipo entero, el valor de origen se redondea hacia cero al valor
entero más cercano, y este valor entero se convierte en el resultado de la conversión. Si el valor entero
resultante está fuera del intervalo del tipo de destino, System.OverflowException se produce una excepción.
En el caso de una conversión de float o double a un tipo entero, el procesamiento depende del contexto
de comprobación de desbordamiento (los operadores comprobados y sin comprobar) en el que tiene lugar
la conversión:
En un checked contexto, la conversión se realiza de la siguiente manera:
Si el valor del operando es NaN o infinito, System.OverflowException se produce una excepción.
De lo contrario, el operando de origen se redondea hacia cero al valor entero más cercano. Si
este valor entero está dentro del intervalo del tipo de destino, este valor es el resultado de la
conversión.
De lo contrario, se produce una excepción System.OverflowException .
En un unchecked contexto, la conversión siempre se realiza correctamente y continúa como se indica
a continuación.
Si el valor del operando es NaN o infinito, el resultado de la conversión es un valor no
especificado del tipo de destino.
De lo contrario, el operando de origen se redondea hacia cero al valor entero más cercano. Si
este valor entero está dentro del intervalo del tipo de destino, este valor es el resultado de la
conversión.
De lo contrario, el resultado de la conversión es un valor no especificado del tipo de destino.
En el caso de una conversión de double a float , el double valor se redondea al float valor más
próximo. Si el double valor es demasiado pequeño para representarlo como un float , el resultado es cero
positivo o cero negativo. Si el double valor es demasiado grande para representarlo como un float , el
resultado se convierte en infinito positivo o infinito negativo. Si el double valor es Nan, el resultado es
también Nan.
En el caso de una conversión de float o double a decimal , el valor de origen se convierte en decimal
representación y se redondea al número más cercano después de la posición decimal 28 si es necesario (el
tipo decimal). Si el valor de origen es demasiado pequeño para representarlo como un decimal , el resultado
se convierte en cero. Si el valor de origen es NaN, infinito o demasiado grande para representarse como
decimal , System.OverflowException se produce una excepción.
En el caso de una conversión de decimal a float o double , el decimal valor se redondea al double valor
o más próximo float . Aunque esta conversión puede perder precisión, nunca provoca que se produzca una
excepción.
Conversiones de enumeración explícitas
Las conversiones de enumeración explícitas son:
De sbyte , byte , short , ushort , int , uint , long , ulong , char , float , double o decimal a
cualquier enum_type.
Desde cualquier enum_type a sbyte , byte , short , ushort , int , uint , long , ulong ,,, char float
double o decimal .
Desde cualquier enum_type a cualquier otro enum_type.
Una conversión de enumeración explícita entre dos tipos se procesa tratando cualquier enum_type participante
como el tipo subyacente de ese enum_type y, a continuación, realizando una conversión numérica implícita o
explícita entre los tipos resultantes. Por ejemplo, dada una enum_type E con y el tipo subyacente de int , una
conversión de E a byte se procesa como una conversión numérica explícita (Conversiones numéricas
explícitas) de int a byte , y una conversión de byte a E se procesa como una conversión numérica implícita
(conversionesnuméricas implícitas) de byte a int .
Conversiones explícitas que aceptan valores NULL
Las conversiones explícitas que aceptan valores NULL permiten conversiones explícitas explícitas que
operan en tipos de valor que no aceptan valores NULL para usarse también con formas que aceptan valores
NULL de esos tipos. Para cada una de las conversiones explícitas predefinidas que convierten de un tipo de valor
que no acepta valores NULL S a un tipo de valor que no acepta valores NULL T (conversión de identidad,
conversiones numéricas implícitas, conversiones de enumeración implícita, Conversiones numéricas explícitasy
conversiones de enumeración explícita), existen las siguientes conversiones que aceptan valores NULL:
Una conversión explícita de S? a T? .
Una conversión explícita de S a T? .
Una conversión explícita de S? a T .
La evaluación de una conversión que acepta valores NULL en función de una conversión subyacente de S a T
continúa como sigue:
Si la conversión que acepta valores NULL es de S? a T? :
Si el valor de origen es null ( HasValue la propiedad es false), el resultado es el valor null de tipo T? .
De lo contrario, la conversión se evalúa como un desajuste de S? a S , seguido de la conversión
subyacente de S a T , seguida de un ajuste de T a T? .
Si la conversión que acepta valores NULL es de S a T? , la conversión se evalúa como la conversión
subyacente de S a T seguida de un ajuste de T a T? .
Si la conversión que acepta valores NULL es de S? a T , la conversión se evalúa como un desajuste de S?
a S seguido de la conversión subyacente de S a T .
Tenga en cuenta que un intento de desencapsular un valor que acepta valores null producirá una excepción si el
valor es null .
Conversiones de referencia explícitas
Las conversiones de referencia explícitas son:
Desde object y dynamic hasta cualquier otro reference_type.
Desde cualquier class_type S a cualquier class_type T , proporcionado S es una clase base de T .
De cualquier class_type S a cualquier interface_type T , proporcionado no S está sellado y se proporciona
no S implementa T .
De cualquier interface_type S a cualquier class_type T , proporcionado T no es una implementación
sellada o proporcionada T S .
Desde cualquier interface_type S a cualquier interface_type T , proporcionado S no se deriva de T .
En un array_type S con un tipo de elemento SE a un array_type T con un tipo de elemento TE , siempre
que se cumplan todas las condiciones siguientes:
S y T solo difieren en el tipo de elemento. En otras palabras, S y T tienen el mismo número de
dimensiones.
SE Y TE son reference_type s.
Existe una conversión de referencia explícita de SE a TE .
Desde System.Array y las interfaces que implementa en cualquier array_type.
Desde un tipo de matriz unidimensional S[] hasta System.Collections.Generic.IList<T> y sus interfaces
base, siempre que haya una conversión de referencia explícita de S a T .
Desde System.Collections.Generic.IList<S> y sus interfaces base a un tipo de matriz unidimensional T[] ,
siempre que haya una conversión explícita de identidad o de referencia de S a T .
Desde System.Delegate y las interfaces que implementa en cualquier delegate_type.
Desde un tipo de referencia a un tipo de referencia T si tiene una conversión de referencia explícita a un
tipo de referencia T0 y T0 tiene una conversión de identidad T .
Desde un tipo de referencia a un tipo de interfaz o delegado T si tiene una conversión de referencia explícita
a una interfaz o un tipo de delegado T0 y T0 se puede convertir en varianza o es convertible en T T T0
(conversión devarianza).
Desde D<S1...Sn> hasta D<T1...Tn> donde D<X1...Xn> es un tipo de delegado genérico, D<S1...Sn> no es
compatible con o es idéntico a D<T1...Tn> , y para cada parámetro Xi de tipo de D los siguientes
elementos:
Si Xi es invariable, Si es idéntico a Ti .
Si Xi es covariante, hay una identidad implícita o explícita o una conversión de referencia de Si a
Ti .
Si Xi es contravariante, Si y Ti son idénticos o ambos tipos de referencia.
Conversiones explícitas que implican parámetros de tipo que se sabe que son tipos de referencia. Para
obtener más información sobre las conversiones explícitas que implican parámetros de tipo, vea
conversiones explícitas que implican parámetros de tipo.
Las conversiones de referencia explícitas son esas conversiones entre los tipos de referencia que requieren
comprobaciones en tiempo de ejecución para asegurarse de que son correctas.
Para que una conversión de referencia explícita se realice correctamente en tiempo de ejecución, el valor del
operando de origen debe ser null , o el tipo real del objeto al que hace referencia el operando de origen debe
ser un tipo que se pueda convertir al tipo de destino mediante una conversión de referencia implícita
(conversiones de referencia implícita) o una conversión boxing (conversiones boxing). Si se produce un error en
una conversión de referencia explícita, System.InvalidCastException se produce una excepción.
Las conversiones de referencia, implícitas o explícitas, nunca cambian la identidad referencial del objeto que se
va a convertir. En otras palabras, aunque una conversión de referencia puede cambiar el tipo de la referencia,
nunca cambia el tipo o valor del objeto al que se hace referencia.
Conversiones unboxing
Una conversión unboxing permite que un tipo de referencia se convierta explícitamente en un value_type. Existe
una conversión unboxing de los tipos object , dynamic y System.ValueType en cualquier
non_nullable_value_type y desde cualquier interface_type a cualquier non_nullable_value_type que implemente
el interface_type. Además, System.Enum se puede aplicar la conversión unboxing a cualquier enum_type.
Existe una conversión unboxing de un tipo de referencia a un nullable_type si existe una conversión unboxing
del tipo de referencia al non_nullable_value_type subyacente de la nullable_type.
Un tipo S de valor tiene una conversión unboxing de un tipo de interfaz I si tiene una conversión unboxing
de un tipo de interfaz I0 y I0 tiene una conversión de identidad en I .
Un tipo S de valor tiene una conversión unboxing de un tipo de interfaz I si tiene una conversión unboxing
de un tipo de interfaz o delegado, I0 y I0 se puede convertir en varianza o ser convertible en I I I0
(conversión devarianza).
Una operación de conversión unboxing consiste en comprobar primero que la instancia de objeto es un valor de
conversión boxing del value_type especificado y, a continuación, copiar el valor fuera de la instancia. Al aplicar la
conversión unboxing a una referencia nula a un nullable_type , se genera el valor null de la nullable_type. Se
puede aplicar la conversión unboxing a un struct del tipo System.ValueType , ya que es una clase base para
todos los Structs (herencia).
Las conversiones unboxing se describen con más detalle en conversiones unboxing.
Conversiones dinámicas explícitas
Existe una conversión dinámica explícita de una expresión de tipo dynamic a cualquier tipo T . La conversión
está enlazada dinámicamente (enlace dinámico), lo que significa que se buscará una conversión explícita en
tiempo de ejecución desde el tipo en tiempo de ejecución de la expresión hasta T . Si no se encuentra ninguna
conversión, se produce una excepción en tiempo de ejecución.
Si no se desea el enlace dinámico de la conversión, la expresión se puede convertir primero a object y, a
continuación, al tipo deseado.
Supongamos que se define la clase siguiente:
class C
{
int i;
object o = "1";
dynamic d = "2";
La mejor conversión de o a C se encuentra en tiempo de compilación para ser una conversión de referencia
explícita. Esto produce un error en tiempo de ejecución, porque "1" no es en realidad C . La conversión de d
a C sin embargo, como una conversión dinámica explícita, se suspende en tiempo de ejecución, donde se
encuentra una conversión definida por el usuario del tipo en tiempo de ejecución de d -- string --a C , y se
realiza correctamente.
Conversiones explícitas que implican parámetros de tipo
Existen las siguientes conversiones explícitas para un parámetro de tipo determinado T :
Desde la clase base efectiva C de T a T y desde cualquier clase base de C a T . En tiempo de ejecución,
si T es un tipo de valor, la conversión se ejecuta como conversión unboxing. De lo contrario, la conversión
se ejecuta como una conversión de referencia explícita o una conversión de identidad.
Desde cualquier tipo de interfaz a T . En tiempo de ejecución, si T es un tipo de valor, la conversión se
ejecuta como conversión unboxing. De lo contrario, la conversión se ejecuta como una conversión de
referencia explícita o una conversión de identidad.
De T a cualquier interface_type I siempre y cuando no haya una conversión implícita de T a I . En
tiempo de ejecución, si T es un tipo de valor, la conversión se ejecuta como una conversión boxing seguida
de una conversión de referencia explícita. De lo contrario, la conversión se ejecuta como una conversión de
referencia explícita o una conversión de identidad.
De un parámetro de tipo U a T , proporcionado T depende de U (restricciones de parámetro detipo). En
tiempo de ejecución, si U es un tipo de valor, T y U son necesariamente el mismo tipo y no se realiza
ninguna conversión. De lo contrario, si T es un tipo de valor, la conversión se ejecuta como conversión
unboxing. De lo contrario, la conversión se ejecuta como una conversión de referencia explícita o una
conversión de identidad.
Si T se sabe que es un tipo de referencia, las conversiones anteriores se clasifican como conversiones de
referencia explícitas (conversiones de referencia explícita). Si T no se sabe que es un tipo de referencia, las
conversiones anteriores se clasifican como conversiones unboxing (conversiones unboxing).
Las reglas anteriores no permiten una conversión explícita directa de un parámetro de tipo sin restricciones a un
tipo que no sea de interfaz, lo que podría ser sorprendente. La razón de esta regla es evitar la confusión y hacer
que la semántica de dichas conversiones sea clara. Por ejemplo, consideremos la siguiente declaración:
class X<T>
{
public static long F(T t) {
return (long)t; // Error
}
}
Si se permitía la conversión directa explícita de t a int , es posible que cabría esperar que X<int>.F(7)
devolvería 7L . Sin embargo, no lo haría, porque las conversiones numéricas estándar solo se tienen en cuenta
cuando se sabe que los tipos son numéricos en tiempo de enlace. Para que la semántica esté clara, en su lugar
se debe escribir en el ejemplo anterior:
class X<T>
{
public static long F(T t) {
return (long)(object)t; // Ok, but will only work when T is long
}
}
Este código se compilará ahora pero la ejecución X<int>.F(7) producirá una excepción en tiempo de ejecución,
ya que una conversión boxing int no se puede convertir directamente a long .
Conversiones explícitas definidas por el usuario
Una conversión explícita definida por el usuario se compone de una conversión explícita estándar opcional,
seguida de una ejecución de un operador de conversión implícito o explícito definido por el usuario, seguida de
otra conversión explícita estándar opcional. Las reglas exactas para evaluar las conversiones explícitas definidas
por el usuario se describen en procesamiento de conversiones explícitas definidas por el usuario.
Conversiones estándar
Las conversiones estándar son las conversiones predefinidas que pueden producirse como parte de una
conversión definida por el usuario.
Conversiones implícitas estándar
Las siguientes conversiones implícitas se clasifican como conversiones implícitas estándar:
Conversiones de identidad (conversión de identidad)
Conversiones numéricas IMPLÍCITAS (Conversiones numéricas implícitas)
Conversiones implícitas que aceptan valores NULL (conversiones implícitas que aceptan valores NULL)
Conversiones de referencia IMPLÍCITAS (conversiones de referencia implícitas)
Conversiones Boxing (conversiones boxing)
Conversiones implícitas de expresiones constantes (conversiones implícitas de expresión constante)
Conversiones implícitas que implican parámetros de tipo (conversiones implícitas que implican parámetros
de tipo)
Las conversiones implícitas estándar excluyen específicamente las conversiones implícitas definidas por el
usuario.
Conversiones explícitas estándar
Las conversiones explícitas estándar son todas las conversiones implícitas estándar más el subconjunto de las
conversiones explícitas para las que existe una conversión implícita estándar opuesta. En otras palabras, si existe
una conversión implícita estándar de un tipo A a un tipo B , existe una conversión explícita estándar del tipo
A al tipo B y del tipo B al tipo A .
Por motivos de brevedad, en esta sección se usa la forma abreviada para los tipos de tarea Task y Task<T>
(funciones asincrónicas).
Una expresión lambda F es compatible con un tipo de árbol de expresión Expression<D> si F es compatible
con el tipo de delegado D . Tenga en cuenta que esto no se aplica a los métodos anónimos, solo a las
expresiones lambda.
Ciertas expresiones lambda no se pueden convertir en tipos de árbol de expresión: aunque la conversión existe,
se produce un error en tiempo de compilación. Este es el caso si la expresión lambda:
Tiene un cuerpo de bloque
Contiene operadores de asignación simples o compuestos
Contiene una expresión enlazada dinámicamente
Async
En los ejemplos siguientes se usa un tipo de delegado genérico Func<A,R> que representa una función que
toma un argumento de tipo A y devuelve un valor de tipo R :
En las asignaciones
Func<int,int> f1 = x => x + 1; // Ok
Func<int,double> f2 = x => x + 1; // Ok
los tipos de valor devuelto y de parámetro de cada función anónima se determinan a partir del tipo de la
variable a la que se asigna la función anónima.
La primera asignación convierte correctamente la función anónima en el tipo de delegado Func<int,int>
porque, cuando x se especifica el tipo int , x+1 es una expresión válida que se convertirá implícitamente al
tipo int .
Del mismo modo, la segunda asignación convierte correctamente la función anónima en el tipo
Func<int,double> de delegado porque el resultado de x+1 (de tipo int ) es implícitamente convertible al tipo
double .
Sin embargo, la tercera asignación es un error en tiempo de compilación porque, cuando x se especifica
double el tipo, el resultado de x+1 (de tipo double ) no se pueden convertir implícitamente al tipo int .
class Test
{
static double[] Apply(double[] a, Function f) {
double[] result = new double[a.Length];
for (int i = 0; i < a.Length; i++) result[i] = f(a[i]);
return result;
}
Dado que los dos delegados de función anónimos tienen el mismo conjunto (vacío) de variables externas
capturadas y como las funciones anónimas son idénticas semánticamente, el compilador puede hacer que los
delegados hagan referencia al mismo método de destino. En realidad, el compilador puede devolver la misma
instancia de delegado de ambas expresiones de función anónimas.
Evaluación de conversiones de funciones anónimas a tipos de árbol de expresión
La conversión de una función anónima en un tipo de árbol de expresión genera un árbol de expresión (tipos de
árbol de expresión). Más concretamente, la evaluación de la conversión de funciones anónimas conduce a la
construcción de una estructura de objeto que representa la estructura de la propia función anónima. La
estructura precisa del árbol de expresión, así como el proceso exacto para crearla, se definen en la
implementación.
Ejemplo de implementación
En esta sección se describe una posible implementación de conversiones de funciones anónimas en términos de
otras construcciones de C#. La implementación que se describe aquí se basa en los mismos principios utilizados
por el compilador de Microsoft C#, pero no es una implementación asignada, ni es lo único posible. Solo se
mencionan brevemente las conversiones a los árboles de expresión, ya que su semántica exacta está fuera del
ámbito de esta especificación.
En el resto de esta sección se proporcionan varios ejemplos de código que contiene funciones anónimas con
diferentes características. En cada ejemplo, se proporciona una traducción correspondiente al código que usa
solo otras construcciones de C#. En los ejemplos, se supone que el identificador D representa el siguiente tipo
de delegado:
La forma más sencilla de una función anónima es aquella que no captura variables externas:
class Test
{
static void F() {
D d = () => { Console.WriteLine("test"); };
}
}
Esto se puede traducir a una creación de instancias de delegado que hace referencia a un método estático
generado por el compilador en el que se coloca el código de la función anónima:
class Test
{
static void F() {
D d = new D(__Method1);
}
En el ejemplo siguiente, la función anónima hace referencia a los miembros de instancia de this :
class Test
{
int x;
void F() {
D d = () => { Console.WriteLine(x); };
}
}
Esto puede traducirse a un método de instancia generado por el compilador que contenga el código de la
función anónima:
class Test
{
int x;
void F() {
D d = new D(__Method1);
}
void __Method1() {
Console.WriteLine(x);
}
}
class Test
{
void F() {
int y = 123;
D d = () => { Console.WriteLine(y); };
}
}
La duración de la variable local debe extenderse ahora a al menos la duración del delegado de función anónima.
Esto puede lograrse mediante la "activación" de la variable local en un campo de una clase generada por el
compilador. La creación de instancias de la variable local (creaciónde instancias de variables locales)
corresponde entonces a la creación de una instancia de la clase generada por el compilador y el acceso a la
variable local corresponde al acceso a un campo en la instancia de la clase generada por el compilador. Además,
la función anónima se convierte en un método de instancia de la clase generada por el compilador:
class Test
{
void F() {
__Locals1 __locals1 = new __Locals1();
__locals1.y = 123;
D d = new D(__locals1.__Method1);
}
class __Locals1
{
public int y;
Por último, la función anónima siguiente captura this y dos variables locales con distintas duraciones:
class Test
{
int x;
void F() {
int y = 123;
for (int i = 0; i < 10; i++) {
int z = i * 2;
D d = () => { Console.WriteLine(x + y + z); };
}
}
}
En este caso, se crea una clase generada por el compilador para cada bloque de instrucciones en el que se
capturan las variables locales de forma que las variables locales de los diferentes bloques puedan tener
duraciones independientes. Una instancia de __Locals2 , la clase generada por el compilador para el bloque de
instrucciones interno, contiene la variable local z y un campo que hace referencia a una instancia de
__Locals1 . Una instancia de __Locals1 , la clase generada por el compilador para el bloque de instrucciones
exterior, contiene la variable local y y un campo que hace referencia this al miembro de función envolvente.
Con estas estructuras de datos es posible llegar a todas las variables externas capturadas a través de una
instancia de __Local2 , y el código de la función anónima se puede implementar como un método de instancia
de esa clase.
class Test
{
void F() {
__Locals1 __locals1 = new __Locals1();
__locals1.__this = this;
__locals1.y = 123;
for (int i = 0; i < 10; i++) {
__Locals2 __locals2 = new __Locals2();
__locals2.__locals1 = __locals1;
__locals2.z = i * 2;
D d = new D(__locals2.__Method1);
}
}
class __Locals1
{
public Test __this;
public int y;
}
class __Locals2
{
public __Locals1 __locals1;
public int z;
También se puede usar la misma técnica que se aplica aquí para capturar variables locales al convertir funciones
anónimas en árboles de expresión: las referencias a los objetos generados por el compilador se pueden
almacenar en el árbol de expresión y el acceso a las variables locales se puede representar como accesos de
campo en estos objetos. La ventaja de este enfoque es que permite compartir las variables locales "levantadas"
entre los delegados y los árboles de expresión.
class Test
{
static string F(object o) {...}
}
}
Los grupos de métodos pueden influir en la resolución de sobrecarga y participar en la inferencia de tipos.
Consulte miembros de función para obtener más detalles.
La evaluación en tiempo de ejecución de una conversión de grupo de métodos continúa como sigue:
Si el método seleccionado en tiempo de compilación es un método de instancia, o es un método de
extensión al que se tiene acceso como un método de instancia, el objeto de destino del delegado se
determina a partir de la expresión de instancia asociada a E :
Se evalúa la expresión de instancia. Si esta evaluación provoca una excepción, no se ejecuta ningún
paso más.
Si la expresión de instancia es de un reference_type, el valor calculado por la expresión de instancia se
convierte en el objeto de destino. Si el método seleccionado es un método de instancia y el objeto de
destino es null , System.NullReferenceException se produce una excepción y no se ejecuta ningún
paso más.
Si la expresión de instancia es de un value_type, se realiza una operación de conversión boxing
(conversiones boxing) para convertir el valor en un objeto y este objeto se convierte en el objeto de
destino.
De lo contrario, el método seleccionado forma parte de una llamada al método estático y el objeto de destino
del delegado es null .
Se asigna una nueva instancia del tipo de delegado D . Si no hay suficiente memoria disponible para asignar
la nueva instancia, System.OutOfMemoryException se produce una excepción y no se ejecuta ningún paso más.
La nueva instancia de delegado se inicializa con una referencia al método que se determinó en tiempo de
compilación y una referencia al objeto de destino calculado anteriormente.
Expresiones
18/09/2021 • 456 minutes to read
Una expresión es una secuencia de operadores y operandos. En este capítulo se define la sintaxis, el orden de
evaluación de los operandos y operadores y el significado de las expresiones.
Clasificaciones de expresiones
Una expresión se clasifica de las siguientes formas:
Un valor. Todos los valores tienen un tipo asociado.
Una variable. Cada variable tiene un tipo asociado, es decir, el tipo declarado de la variable.
Espacio de nombres. Una expresión con esta clasificación solo puede aparecer como la parte izquierda de un
member_access (acceso a miembros). En cualquier otro contexto, una expresión clasificada como un espacio
de nombres produce un error en tiempo de compilación.
Un tipo. Una expresión con esta clasificación solo puede aparecer como el lado izquierdo de un
member_access (acceso a miembros) o como un operando para el as operador (el operador as), el is
operador (operador is) o el typeof operador (el operador typeof). En cualquier otro contexto, una expresión
clasificada como un tipo genera un error en tiempo de compilación.
Un grupo de métodos, que es un conjunto de métodos sobrecargados que resultan de una búsqueda de
miembros (búsqueda de miembros). Un grupo de métodos puede tener una expresión de instancia asociada
y una lista de argumentos de tipo asociada. Cuando se invoca un método de instancia, el resultado de
evaluar la expresión de instancia se convierte en la instancia representada por this (este acceso). Se
permite un grupo de métodos en un invocation_expression (expresiones de invocación), un
delegate_creation_expression (expresiones de creación de delegado) y como el lado izquierdo de un
operador is y se puede convertir implícitamente en un tipo de delegado compatible (conversiones de grupo
de métodos). En cualquier otro contexto, una expresión clasificada como grupo de métodos produce un error
en tiempo de compilación.
Un literal null. Una expresión con esta clasificación se puede convertir implícitamente a un tipo de referencia
o a un tipo que acepta valores NULL.
Una función anónima. Una expresión con esta clasificación se puede convertir implícitamente en un tipo de
delegado compatible o un tipo de árbol de expresión.
Un acceso de propiedad. Cada acceso de propiedad tiene un tipo asociado, es decir, el tipo de la propiedad.
Además, un acceso de propiedad puede tener una expresión de instancia asociada. Cuando se invoca un
descriptor get set de acceso (el bloque o) de una propiedad de instancia, el resultado de evaluar la
expresión de instancia se convierte en la instancia representada por this (este acceso).
Un acceso de evento. Cada acceso a eventos tiene un tipo asociado, es decir, el tipo del evento. Además, un
acceso a eventos puede tener una expresión de instancia asociada. Un acceso a eventos puede aparecer
como el operando izquierdo de += los -= operadores y (asignación de eventos). En cualquier otro contexto,
una expresión clasificada como acceso de evento produce un error en tiempo de compilación.
Un acceso de indexador. Cada acceso de indexador tiene un tipo asociado, es decir, el tipo de elemento del
indexador. Además, un acceso de indexador tiene una expresión de instancia asociada y una lista de
argumentos asociada. Cuando se invoca un descriptor get set de acceso (el bloque o) de un acceso de
indexador, el resultado de evaluar la expresión de instancia se convierte en la instancia representada por
this (este acceso) y el resultado de evaluar la lista de argumentos se convierte en la lista de parámetros de
la invocación.
Nada. Esto sucede cuando la expresión es una invocación de un método con un tipo de valor devuelto de
void . Una expresión clasificada como Nothing solo es válida en el contexto de una statement_expression
(instrucciones de expresión).
El resultado final de una expresión nunca es un espacio de nombres, tipo, grupo de métodos o acceso a eventos.
En su lugar, como se indicó anteriormente, estas categorías de expresiones son construcciones intermedias que
solo se permiten en determinados contextos.
Un acceso de propiedad o de indexador siempre se reclasifica como un valor realizando una invocación del
descriptor de acceso get o del descriptor de acceso set. El descriptor de acceso concreto viene determinado por
el contexto de la propiedad o el acceso del indexador: Si el acceso es el destino de una asignación, se invoca al
descriptor de acceso set para asignar un nuevo valor (asignación simple). De lo contrario, el descriptor de acceso
get se invoca para obtener el valor actual (valores de las expresiones).
Valores de las expresiones
En última instancia, la mayoría de las construcciones que implican una expresión requieren que la expresión
denote un valor . En tales casos, si la expresión real denota un espacio de nombres, un tipo, un grupo de
métodos o nada, se produce un error en tiempo de compilación. Sin embargo, si la expresión denota un acceso
de propiedad, un acceso a indexador o una variable, el valor de la propiedad, el indexador o la variable se
sustituyen implícitamente:
El valor de una variable es simplemente el valor almacenado actualmente en la ubicación de almacenamiento
identificada por la variable. Una variable se debe considerar definitivamente asignada (asignación definitiva)
antes de que se pueda obtener su valor o, de lo contrario, se producirá un error en tiempo de compilación.
El valor de una expresión de acceso de propiedad se obtiene invocando el descriptor de acceso get de la
propiedad. Si la propiedad no tiene un descriptor de acceso get, se produce un error en tiempo de
compilación. De lo contrario, se realiza una invocación de miembro de función (comprobación en tiempo de
compilación de la resolución dinámica de sobrecarga) y el resultado de la invocación se convierte en el valor
de la expresión de acceso de propiedad.
El valor de una expresión de acceso de indexador se obtiene al invocar el descriptor de acceso get del
indexador. Si el indizador no tiene ningún descriptor de acceso get, se produce un error en tiempo de
compilación. De lo contrario, se realiza una invocación de un miembro de función (comprobación en tiempo
de compilación de la resolución dinámica de sobrecarga) con la lista de argumentos asociada a la expresión
de acceso del indizador y el resultado de la invocación se convierte en el valor de la expresión de acceso del
indexador.
object o = 5;
dynamic d = 5;
Operadores
Las expresiones se construyen a partir de los operadores*operandos _ y. Los operadores de una expresión
indican qué operaciones se aplican a los operandos. Ejemplos de operadores incluyen + , - , _ , / y new .
Algunos ejemplos de operandos son literales, campos, variables locales y expresiones.
Hay tres tipos de operadores:
Operadores unarios. Los operadores unarios toman un operando y usan la notación de prefijo (como --x )
o la notación de postfijo (como x++ ).
Operadores binarios. Los operadores binarios toman dos operandos y todos usan la notación infija (como
x + y ).
Operador ternario. Solo un operador ternario, ?: , existe; toma tres operandos y usa la notación de infijo (
c ? x : y ).
El orden de evaluación de los operadores de una expresión viene determinado por la precedencia _ and _
asociativity de los operadores (precedencia y asociatividad del operador).
Los operandos de una expresión se evalúan de izquierda a derecha. Por ejemplo, en F(i) + G(i++) * H(i) , se
llama al método con F el valor anterior de i , a continuación, se llama al método G con el valor anterior de
i , y, por último, H se llama al método con el nuevo valor de i . Esto es independiente de la prioridad de
operador y no está relacionada con ella.
Algunos operadores se pueden sobrecargar . La sobrecarga de operadores permite especificar
implementaciones de operador definidas por el usuario para las operaciones donde uno o ambos operandos
son de una clase definida por el usuario o un tipo de estructura (sobrecarga de operadores).
Prioridad y asociatividad de los operadores
Cuando una expresión contiene varios operadores, el *precedencia _ de los operadores controla el orden en el
que se evalúan los operadores individuales. Por ejemplo, la expresión x + y _ z se evalúa como x + (y * z)
porque el operador * tiene mayor precedencia que el operador binario + . La precedencia de un operador se
establece mediante la definición de su producción gramatical asociada. Por ejemplo, un additive_expression se
compone de una secuencia de multiplicative_expression s separadas + por - operadores or, lo que permite
que los operadores + y tengan - menor prioridad que los * / operadores, y % .
En la tabla siguiente se resumen todos los operadores en orden de prioridad, de mayor a menor:
Operadores relacionales y de prueba Comprobación de tipos y relacional < > <= >= is as
de tipos
Cuando un operando se encuentra entre dos operadores con la misma precedencia, la asociatividad de los
operadores controla el orden en que se realizan las operaciones:
Excepto en el caso de los operadores de asignación y el operador de uso combinado de NULL, todos los
operadores binarios son asociativos a la izquierda, lo que significa que las operaciones se realizan de
izquierda a derecha. Por ejemplo, x + y + z se evalúa como (x + y) + z .
Los operadores de asignación, el operador de uso combinado de NULL y el operador condicional ( ?: ) son
asociativos a la derecha, lo que significa que las operaciones se realizan de derecha a izquierda. Por
ejemplo, x = y = z se evalúa como x = (y = z) .
La precedencia y la asociatividad pueden controlarse mediante paréntesis. Por ejemplo, x + y * z primero
multiplica y por z y luego suma el resultado a x , pero (x + y) * z primero suma x y y y luego
multiplica el resultado por z .
Sobrecarga de operadores
Todos los operadores unarios y binarios tienen implementaciones predefinidas que están disponibles
automáticamente en cualquier expresión. Además de las implementaciones predefinidas, las implementaciones
definidas por el usuario se pueden introducir incluyendo operator declaraciones en clases y Structs
(operadores). Las implementaciones de operador definidas por el usuario siempre tienen prioridad sobre las
implementaciones de operadores predefinidas: solo cuando no existan implementaciones de operadores
definidos por el usuario aplicables, se tendrán en cuenta las implementaciones de operadores predefinidas,
como se describe en resolución de sobrecargas de operador unario y resolución de sobrecarga de operadores
binarios.
Los operadores unarios sobrecargables son:
+ - ! ~ ++ -- true false
Aunque true y false no se utilizan explícitamente en expresiones (y, por consiguiente, no se incluyen en la
tabla de precedencia en la precedencia y asociatividadde los operadores), se consideran operadores porque se
invocan en varios contextos de expresión: Expresiones booleanas (Expresiones booleanas) y expresiones que
implican al operador condicional (operador condicional) y operadores lógicos condicionales (operadores lógicos
condicionales)
Los operadores binarios sobrecargables son:
Solo se pueden sobrecargar los operadores enumerados anteriormente. En concreto, no es posible sobrecargar
el acceso a miembros, la invocación de métodos o los = && operadores,,,,,,,,,,, || ?? ?: => checked
unchecked new typeof default as y is .
op x operator op(x)
N OTA C IÓ N DE O P ERA DO R N OTA C IÓ N F UN C IO N A L
x op operator op(x)
x op y operator op(x,y)
Las declaraciones de operador definidas por el usuario siempre requieren que al menos uno de los parámetros
sea del tipo de clase o estructura que contiene la declaración de operador. Por lo tanto, no es posible que un
operador definido por el usuario tenga la misma firma que un operador predefinido.
Las declaraciones de operador definidas por el usuario no pueden modificar la sintaxis, la precedencia o la
asociatividad de un operador. Por ejemplo, el / operador siempre es un operador binario, siempre tiene el nivel
de prioridad especificado en precedencia y asociatividadde los operadores y siempre es asociativo a la izquierda.
Aunque es posible que un operador definido por el usuario realice cualquier cálculo, se desaconsejan las
implementaciones que generan resultados distintos de los que se esperaban de forma intuitiva. Por ejemplo,
una implementación de operator == debería comparar los dos operandos para determinar si son iguales y
devolver un bool resultado adecuado.
Las descripciones de operadores individuales en expresiones primarias a través de operadores lógicos
condicionales especifican las implementaciones predefinidas de los operadores y cualquier regla adicional que
se aplique a cada operador. En las descripciones se usa la *resolución de sobrecargas del operador unario***, la
_resolución de sobrecarga del operador binario*, y _ valores de promoción *, las definiciones de que se
encuentran en las secciones siguientes.
Resolución de sobrecarga del operador unario
Una operación con el formato op x o x op , donde op es un operador unario sobrecargable y x es una
expresión de tipo X , se procesa de la siguiente manera:
El conjunto de operadores candidatos definidos por el usuario que proporciona X para la operación
operator op(x) se determina mediante las reglas de los operadores candidatos definidospor el usuario.
Si el conjunto de operadores definidos por el usuario candidatos no está vacío, se convierte en el conjunto de
operadores candidatos para la operación. De lo contrario, las implementaciones unarias predefinidas
operator op , incluidas sus formas de elevación, se convierten en el conjunto de operadores candidatos para
la operación. Las implementaciones predefinidas de un operador determinado se especifican en la
descripción del operador (expresiones primarias y operadores unarios).
Las reglas de resolución de sobrecarga de la resolución de sobrecarga se aplican al conjunto de operadores
candidatos para seleccionar el mejor operador con respecto a la lista de argumentos (x) , y este operador
se convierte en el resultado del proceso de resolución de sobrecarga. Si la resolución de sobrecarga no
selecciona un solo operador mejor, se produce un error en tiempo de enlace.
Resolución de sobrecarga del operador binario
Una operación con el formato x op y , donde op es un operador binario sobrecargable, x es una expresión
de tipo X y y es una expresión de tipo Y , que se procesa de la siguiente manera:
Se determina el conjunto de operadores candidatos definidos por el usuario que proporciona X y Y para la
operación operator op(x,y) . El conjunto consta de la Unión de los operadores candidatos proporcionados
por X y los operadores candidatos proporcionados por, cada uno de los cuales se Y determina mediante
las reglas de los operadores candidatos definidospor el usuario. Si X y Y son del mismo tipo, o si X y Y
se derivan de un tipo base común, los operadores candidatos compartidos solo se producen en el conjunto
combinado una vez.
Si el conjunto de operadores definidos por el usuario candidatos no está vacío, se convierte en el conjunto de
operadores candidatos para la operación. De lo contrario, las implementaciones binarias predefinidas
operator op , incluidas sus formas de elevación, se convierten en el conjunto de operadores candidatos para
la operación. Las implementaciones predefinidas de un operador determinado se especifican en la
descripción del operador (operadores aritméticos a través de operadores lógicos condicionales). En el caso
de los operadores de enumeración y delegado predefinidos, los únicos operadores considerados son los
definidos por un tipo de delegado o de enumeración que es el tipo en tiempo de enlace de uno de los
operandos.
Las reglas de resolución de sobrecarga de la resolución de sobrecarga se aplican al conjunto de operadores
candidatos para seleccionar el mejor operador con respecto a la lista de argumentos (x,y) , y este operador
se convierte en el resultado del proceso de resolución de sobrecarga. Si la resolución de sobrecarga no
selecciona un solo operador mejor, se produce un error en tiempo de enlace.
Operadores candidatos definidos por el usuario
Dado un tipo T y una operación operator op(A) , donde op es un operador sobrecargable y A es una lista de
argumentos, el conjunto de operadores definidos por el usuario candidatos que proporciona T for
operator op(A) se determina de la manera siguiente:
Determine el tipo T0 . Si T es un tipo que acepta valores NULL, T0 es su tipo subyacente, de lo contrario,
T0 es igual a T .
Para todas las operator op declaraciones de T0 y todas las formas de elevación de estos operadores, si al
menos un operador es aplicable (miembro de función aplicable) con respecto a la lista de argumentos A , el
conjunto de operadores candidatos está formado por todos los operadores aplicables en T0 .
De lo contrario, si T0 es object , el conjunto de operadores candidatos está vacío.
De lo contrario, el conjunto de operadores candidatos proporcionado por T0 es el conjunto de operadores
candidatos proporcionado por la clase base directa de T0 , o la clase base efectiva de T0 si T0 es un
parámetro de tipo.
Promociones numéricas
La promoción numérica consiste en realizar automáticamente determinadas conversiones implícitas de los
operandos de los operadores binarios y unarios predefinidos. La promoción numérica no es un mecanismo
distinto, sino un efecto de aplicar la resolución de sobrecarga a los operadores predefinidos. La promoción
numérica específicamente no afecta a la evaluación de operadores definidos por el usuario, aunque se pueden
implementar operadores definidos por el usuario para mostrar efectos similares.
Como ejemplo de promoción numérica, tenga en cuenta las implementaciones predefinidas del * operador
binario:
Cuando se aplican las reglas de resolución de sobrecarga (resolución de sobrecarga) a este conjunto de
operadores, el efecto es seleccionar el primero de los operadores para los que existen conversiones implícitas
desde los tipos de operando. Por ejemplo, para la operación b * s , donde b es byte y s es short , la
resolución de sobrecarga selecciona operator *(int,int) como el mejor operador. Por lo tanto, el efecto es que
b y s se convierten en y int el tipo del resultado es int . Del mismo modo, para la operación i * d ,
donde i es int y d es double , la resolución de sobrecarga selecciona operator *(double,double) como el
mejor operador.
Promociones numéricas unarias
La promoción numérica unaria se produce para los operandos de los + - operadores unarios predefinidos, y
~ . La promoción numérica unaria simplemente consiste en convertir operandos de tipo sbyte , byte , short
, ushort o char en tipo int . Además, para el operador unario - , la promoción numérica unaria convierte
operandos de tipo uint al tipo long .
Promociones numéricas binarias
La promoción numérica binaria se produce para los operandos de los + - * / % & | ^ == != > <
>= <= operadores binarios predefinidos,,,,,,,,,,,, y. La promoción numérica binaria convierte implícitamente
ambos operandos en un tipo común que, en el caso de los operadores no relacionales, también se convierte en
el tipo de resultado de la operación. La promoción numérica binaria consiste en aplicar las siguientes reglas, en
el orden en que aparecen aquí:
Si alguno de los operandos es de tipo decimal , el otro operando se convierte al tipo decimal , o se produce
un error en tiempo de enlace si el otro operando es de tipo float o double .
De lo contrario, si alguno de los operandos es de tipo double , el otro operando se convierte al tipo double .
De lo contrario, si alguno de los operandos es de tipo float , el otro operando se convierte al tipo float .
De lo contrario, si alguno de los operandos es de tipo ulong , el otro operando se convierte al tipo ulong , o
se produce un error en tiempo de enlace si el otro operando es de tipo sbyte , short , int o long .
De lo contrario, si alguno de los operandos es de tipo long , el otro operando se convierte al tipo long .
De lo contrario, si alguno de los operandos es de tipo uint y el otro operando es de tipo sbyte , short o
int , ambos operandos se convierten al tipo long .
De lo contrario, si alguno de los operandos es de tipo uint , el otro operando se convierte al tipo uint .
De lo contrario, ambos operandos se convierten al tipo int .
Tenga en cuenta que la primera regla no permite ninguna operación que mezcle el decimal tipo con double los
float tipos y. La regla sigue el hecho de que no hay conversiones implícitas entre el decimal tipo y los double
float tipos y.
Tenga en cuenta también que no es posible que un operando sea de tipo ulong cuando el otro operando es de
un tipo entero con signo. La razón es que no existe ningún tipo entero que pueda representar el intervalo
completo de ulong , así como los tipos enteros con signo.
En los dos casos anteriores, se puede usar una expresión de conversión para convertir explícitamente un
operando en un tipo que sea compatible con el otro operando.
En el ejemplo
se produce un error en tiempo de enlace porque decimal no se puede multiplicar por double . El error se
resuelve convirtiendo explícitamente el segundo operando en decimal , como se indica a continuación:
Operadores de elevación
Los operadores de elevación permiten a los operadores predefinidos y definidos por el usuario que operan
en tipos de valor que no aceptan valores NULL usarse también con formas que aceptan valores NULL de esos
tipos. Los operadores de elevación se construyen a partir de operadores predefinidos y definidos por el usuario
que cumplen ciertos requisitos, como se describe a continuación:
Para los operadores unarios
+ ++ - -- ! ~
existe una forma de elevación de un operador si el operando y los tipos de resultado son tipos de valor
que no aceptan valores NULL. La forma de elevación se construye agregando un único ? modificador al
operando y a los tipos de resultado. El operador de elevación genera un valor NULL si el operando es
NULL. De lo contrario, el operador de elevación desencapsula el operando, aplica el operador subyacente
y ajusta el resultado.
Para los operadores binarios
existe una forma de elevación de un operador si el operando y los tipos de resultado son todos tipos de
valor que no aceptan valores NULL. La forma de elevación se construye agregando un único ?
modificador a cada operando y tipo de resultado. El operador de elevación genera un valor NULL si uno o
los dos operandos son NULL (una excepción son los & | operadores y del bool? tipo, como se
describe en operadores lógicos booleanos). De lo contrario, el operador de elevación desencapsula los
operandos, aplica el operador subyacente y ajusta el resultado.
Para los operadores de igualdad
== !=
existe una forma de elevación de un operador si los tipos de operando son tipos de valor que no aceptan
valores NULL y si el tipo de resultado es bool . La forma de elevación se construye agregando un único
? modificador a cada tipo de operando. El operador de elevación considera que dos valores NULL son
iguales y un valor NULL es distinto de cualquier valor distinto de NULL. Si ambos operandos no son
NULL, el operador de elevación desencapsula los operandos y aplica el operador subyacente para
generar el bool resultado.
Para los operadores relacionales
existe una forma de elevación de un operador si los tipos de operando son tipos de valor que no aceptan
valores NULL y si el tipo de resultado es bool . La forma de elevación se construye agregando un único
? modificador a cada tipo de operando. El operador de elevación produce el valor false si uno o los
dos operandos son NULL. De lo contrario, el operador de elevación desencapsula los operandos y aplica
el operador subyacente para generar el bool resultado.
Búsqueda de miembros
Una búsqueda de miembros es el proceso por el que se determina el significado de un nombre en el contexto
de un tipo. Una búsqueda de miembros se puede producir como parte de la evaluación de una simple_name
(nombres simples) o un member_access (acceso a miembros) en una expresión. Si el simple_name o
member_access se produce como primary_expression de un invocation_expression (invocaciones de método),
se dice que el miembro se invoca.
Si un miembro es un método o un evento, o si es una constante, un campo o una propiedad de un tipo de
delegado (delegados) o del tipo dynamic (el tipo dinámico), se dice que el miembro es invocable.
La búsqueda de miembros considera no solo el nombre de un miembro, sino también el número de parámetros
de tipo que tiene el miembro y si el miembro es accesible. En lo que respecta a la búsqueda de miembros, los
métodos genéricos y los tipos genéricos anidados tienen el número de parámetros de tipo indicado en sus
declaraciones respectivas y todos los demás miembros tienen parámetros de tipo cero.
Una búsqueda de miembro de un nombre N con K parámetros de tipo en un tipo T se procesa de la
siguiente manera:
En primer lugar, se determina un conjunto de miembros accesibles denominados N :
Si T es un parámetro de tipo, el conjunto es la Unión de los conjuntos de miembros accesibles
denominados N en cada uno de los tipos especificados como restricción principal o restricción
secundaria (restricciones de parámetro de tipo) para T , junto con el conjunto de miembros
accesibles denominados N en object .
De lo contrario, el conjunto se compone de todos los miembros accesibles (acceso a miembros)
denominados N en T , incluidos los miembros heredados y los miembros accesibles denominados
N en object . Si T es un tipo construido, el conjunto de miembros se obtiene sustituyendo los
argumentos de tipo tal y como se describe en miembros de tipos construidos. Los miembros que
incluyen un override modificador se excluyen del conjunto.
Después, si K es cero, se quitan todos los tipos anidados cuyas declaraciones incluyen parámetros de tipo.
Si K no es cero, se quitan todos los miembros con un número diferente de parámetros de tipo. Tenga en
cuenta que cuando K es cero, no se quitan los métodos que tienen parámetros de tipo, ya que el proceso de
inferencia de tipos (inferencia de tipos) podría inferir los argumentos de tipo.
Después, si se invoca el miembro, todos los miembros que no sean de invocable se quitarán del conjunto.
Después, los miembros que están ocultos por otros miembros se quitan del conjunto. Para cada miembro del
S.M conjunto, donde S es el tipo en el que M se declara el miembro, se aplican las siguientes reglas:
Si M es una constante, un campo, una propiedad, un evento o un miembro de enumeración, todos los
miembros declarados en un tipo base de S se quitan del conjunto.
Si M es una declaración de tipos, todos los tipos que no sean declarados en un tipo base de S se
quitan del conjunto y todas las declaraciones de tipos con el mismo número de parámetros de tipo M
declarados en un tipo base de S se quitan del conjunto.
Si M es un método, todos los miembros que no sean de método declarados en un tipo base de S se
quitan del conjunto.
Después, los miembros de interfaz que están ocultos por miembros de clase se quitan del conjunto. Este
paso solo tiene efecto si T es un parámetro de tipo y T tiene una clase base efectiva distinta de object y
un conjunto de interfaces efectivo que no está vacío (restricciones de parámetro de tipo). Para cada miembro
del S.M conjunto, donde S es el tipo en el que se declara el miembro M , se aplican las reglas siguientes si
S es una declaración de clase distinta de object :
Si M es una constante, un campo, una propiedad, un evento, un miembro de enumeración o una
declaración de tipos, todos los miembros declarados en una declaración de interfaz se quitan del
conjunto.
Si M es un método, se quitan del conjunto todos los miembros que no son de método declarados en
una declaración de interfaz y todos los métodos con la misma firma que M se declaran en una
declaración de interfaz se quitan del conjunto.
Por último, si se han quitado los miembros ocultos, se determina el resultado de la búsqueda:
Si el conjunto está formado por un único miembro que no es un método, este miembro es el
resultado de la búsqueda.
De lo contrario, si el conjunto solo contiene métodos, este grupo de métodos es el resultado de la
búsqueda.
De lo contrario, la búsqueda es ambigua y se produce un error en tiempo de enlace.
Para búsquedas de miembros en tipos que no sean de tipo e interfaces, y búsquedas de miembros en interfaces
que son estrictamente de herencia única (cada interfaz de la cadena de herencia tiene exactamente cero o una
interfaz base directa), el efecto de las reglas de búsqueda es simplemente que los miembros derivados ocultan
los miembros base con el mismo nombre o signatura. Dichas búsquedas de herencia única nunca son
ambiguas. Las ambigüedades que pueden surgir en las búsquedas de miembros en interfaces de herencia
múltiple se describen en acceso a miembros de interfaz.
Tipos base
En lo que respecta a la búsqueda de miembros, T se considera que un tipo tiene los siguientes tipos base:
Si T es object , T no tiene ningún tipo base.
Si T es un enum_type, los tipos base de T son los tipos de clase System.Enum , System.ValueType y object
.
Si T es un struct_type, los tipos base de T son los tipos de clase System.ValueType y object .
Si T es un class_type, los tipos base de T son las clases base de T , incluido el tipo de clase object .
Si T es un interface_type, los tipos base de T son las interfaces base de T y el tipo de clase object .
Si T es un array_type, los tipos base de T son los tipos de clase System.Array y object .
Si T es un delegate_type, los tipos base de T son los tipos de clase System.Delegate y object .
Miembros de función
Los miembros de función son miembros que contienen instrucciones ejecutables. Los miembros de función
siempre son miembros de tipos y no pueden ser miembros de espacios de nombres. C# define las siguientes
categorías de miembros de función:
Métodos
Propiedades
Events
Indizadores
Operadores definidos por el usuario
Constructores de instancias
Constructores estáticos
Destructores
A excepción de los destructores y los constructores estáticos (que no se pueden invocar explícitamente), las
instrucciones contenidas en miembros de función se ejecutan a través de las invocaciones de miembros de
función. La sintaxis real para escribir una invocación de miembros de función depende de la categoría de
miembro de función determinada.
La lista de argumentos (listas de argumentos) de una invocación de miembros de función proporciona valores
reales o referencias de variable para los parámetros del miembro de función.
Las invocaciones de métodos genéricos pueden emplear la inferencia de tipos para determinar el conjunto de
argumentos de tipo que se van a pasar al método. Este proceso se describe en inferencia de tipos.
Las invocaciones de métodos, indizadores, operadores y constructores de instancia emplean la resolución de
sobrecarga para determinar cuál de un conjunto candidato de miembros de función se va a invocar. Este
proceso se describe en resolución de sobrecarga.
Una vez que se ha identificado un miembro de función determinado en tiempo de enlace, posiblemente a través
de la resolución de sobrecarga, el proceso real en tiempo de ejecución de la invocación del miembro de función
se describe en la comprobación en tiempo de compilación de la resolución de sobrecarga dinámica.
En la tabla siguiente se resume el procesamiento que tiene lugar en construcciones que implican las seis
categorías de miembros de función que se pueden invocar explícitamente. En la tabla,,, e x y y value
indican expresiones clasificadas como variables o valores, T indica una expresión clasificada como un tipo, F
es el nombre simple de un método y P es el nombre simple de una propiedad.
Listas de argumentos
Cada invocación de miembro de función y delegado incluye una lista de argumentos que proporciona valores
reales o referencias de variable para los parámetros del miembro de función. La sintaxis para especificar la lista
de argumentos de una invocación de miembros de función depende de la categoría de miembros de función:
En el caso de los constructores de instancias, los métodos, los indizadores y los delegados, los argumentos se
especifican como un argument_list, como se describe a continuación. En el caso de los indizadores, al invocar
el set descriptor de acceso, la lista de argumentos incluye también la expresión especificada como
operando derecho del operador de asignación.
En el caso de las propiedades, la lista de argumentos está vacía al invocar el get descriptor de acceso y se
compone de la expresión especificada como operando derecho del operador de asignación al invocar el set
descriptor de acceso.
En el caso de los eventos, la lista de argumentos se compone de la expresión especificada como operando
derecho del += -= operador o.
En el caso de los operadores definidos por el usuario, la lista de argumentos se compone del operando único
del operador unario o de los dos operandos del operador binario.
Los argumentos de las propiedades (propiedades), los eventos (eventos) y los operadores definidos por el
usuario (operadores) siempre se pasan como parámetros de valor (parámetros de valor). Los argumentos de los
indizadores (indizadores) siempre se pasan como parámetros de valor (parámetros de valor) o como matrices
de parámetros (matrices deparámetros). Los parámetros de referencia y de salida no se admiten para estas
categorías de miembros de función.
Los argumentos de una invocación de constructor de instancia, método, indexador o delegado se especifican
como un argument_list:
argument_list
: argument (',' argument)*
;
argument
: argument_name? argument_value
;
argument_name
: identifier ':'
;
argument_value
: expression
| 'ref' variable_reference
| 'out' variable_reference
;
Una argument_list está formada por uno o más argumentos, separados por comas. Cada argumento consta de
un argument_name opcional seguido de un argument_value. Un argumento con un argument_name se conoce
como un argumento *con nombre , mientras que un argumento * sin un argument_name es un argumento
posicional *. Es un error que un argumento posicional aparezca después de un argumento con nombre en un
_argument_list *.
El argument_value puede adoptar uno de los siguientes formatos:
Una expresión, que indica que el argumento se pasa como parámetro de valor (parámetros de valor).
Palabra clave ref seguida de un variable_reference (referencias de variable), que indica que el argumento se
pasa como parámetro de referencia (parámetros de referencia). Una variable debe estar asignada
definitivamente (asignación definitiva) antes de que se pueda pasar como un parámetro de referencia.
Palabra clave out seguida de un variable_reference (referencias de variable), que indica que el argumento se
pasa como parámetro de salida (parámetros de salida). Una variable se considera asignada definitivamente
(asignación definitiva) después de una invocación de miembro de función en la que se pasa la variable como
parámetro de salida.
Parámetros correspondientes
Para cada argumento de una lista de argumentos debe haber un parámetro correspondiente en el miembro de
función o el delegado que se va a invocar.
La lista de parámetros que se utiliza en lo siguiente se determina de la siguiente manera:
En el caso de los métodos virtuales y los indizadores definidos en las clases, la lista de parámetros se elige de
la declaración o invalidación más específica del miembro de función, empezando por el tipo estático del
receptor y buscando en sus clases base.
En el caso de los métodos e indexadores de la interfaz, la lista de parámetros se selecciona como la definición
más específica del miembro, empezando por el tipo de interfaz y buscando en las interfaces base. Si no se
encuentra ninguna lista de parámetros única, se construye una lista de parámetros con nombres inaccesibles
y ningún parámetro opcional, de modo que las invocaciones no pueden usar parámetros con nombre u
omitir argumentos opcionales.
Para los métodos parciales, se usa la lista de parámetros de la declaración de método parcial de definición.
En el caso de todos los demás miembros de función y delegados, solo hay una lista de parámetros única, que
es la que se usa.
La posición de un argumento o parámetro se define como el número de argumentos o parámetros que lo
preceden en la lista de argumentos o en la lista de parámetros.
Los parámetros correspondientes para los argumentos de miembro de función se establecen de la siguiente
manera:
Argumentos en el argument_list de constructores de instancia, métodos, indizadores y delegados:
Un argumento posicional en el que se produce un parámetro fijo en la misma posición en la lista de
parámetros corresponde a ese parámetro.
Un argumento posicional de un miembro de función con una matriz de parámetros invocada en su
forma normal corresponde a la matriz de parámetros, que debe aparecer en la misma posición en la
lista de parámetros.
Argumento posicional de un miembro de función con una matriz de parámetros invocada en su forma
expandida, donde no se produce ningún parámetro fijo en la misma posición en la lista de
parámetros, corresponde a un elemento de la matriz de parámetros.
Un argumento con nombre corresponde al parámetro con el mismo nombre en la lista de parámetros.
En el caso de los indizadores, al invocar el set descriptor de acceso, la expresión especificada como
operando derecho del operador de asignación corresponde al value parámetro implícito de la set
declaración del descriptor de acceso.
En el caso de las propiedades, al invocar el get descriptor de acceso no hay ningún argumento. Al invocar el
set descriptor de acceso, la expresión especificada como operando derecho del operador de asignación
corresponde al value parámetro implícito de la set declaración del descriptor de acceso.
En el caso de los operadores unarios definidos por el usuario (incluidas las conversiones), el único operando
corresponde al parámetro único de la declaración del operador.
En el caso de los operadores binarios definidos por el usuario, el operando izquierdo corresponde al primer
parámetro y el operando derecho se corresponde con el segundo parámetro de la declaración del operador.
Evaluación en tiempo de ejecución de listas de argumentos
Durante el procesamiento en tiempo de ejecución de una invocación de miembro de función (comprobación en
tiempo de compilación de la resolución dinámica de sobrecarga), las expresiones o referencias de variables de
una lista de argumentos se evalúan en orden, de izquierda a derecha, de la manera siguiente:
Para un parámetro de valor, se evalúa la expresión de argumento y se realiza una conversión implícita
(conversiones implícitas) en el tipo de parámetro correspondiente. El valor resultante se convierte en el valor
inicial del parámetro de valor en la invocación del miembro de función.
Para un parámetro de referencia o de salida, se evalúa la referencia de la variable y la ubicación de
almacenamiento resultante se convierte en la ubicación de almacenamiento representada por el parámetro
en la invocación del miembro de función. Si la referencia de variable dada como parámetro de referencia o
de salida es un elemento de matriz de un reference_type, se realiza una comprobación en tiempo de
ejecución para asegurarse de que el tipo de elemento de la matriz es idéntico al tipo del parámetro. Si se
produce un error en esta comprobación, System.ArrayTypeMismatchException se produce una excepción.
Los métodos, indizadores y constructores de instancias pueden declarar su parámetro situado más a la derecha
para que sea una matriz de parámetros (matrices de parámetros). Estos miembros de función se invocan en su
forma normal o en su forma expandida, en función de cuál sea aplicable (miembro de función aplicable):
Cuando se invoca un miembro de función con una matriz de parámetros en su forma normal, el argumento
dado para la matriz de parámetros debe ser una expresión única que se pueda convertir implícitamente
(conversiones implícitas) al tipo de matriz de parámetros. En este caso, la matriz de parámetros actúa
exactamente como un parámetro de valor.
Cuando se invoca un miembro de función con una matriz de parámetros en su forma expandida, la
invocación debe especificar cero o más argumentos posicionales para la matriz de parámetros, donde cada
argumento es una expresión que es convertible implícitamente (conversiones implícitas) al tipo de elemento
de la matriz de parámetros. En este caso, la invocación crea una instancia del tipo de matriz de parámetros
con una longitud correspondiente al número de argumentos, inicializa los elementos de la instancia de la
matriz con los valores de argumento especificados y utiliza la instancia de matriz recién creada como
argumento real.
Las expresiones de una lista de argumentos siempre se evalúan en el orden en que se escriben. Por lo tanto, el
ejemplo
class Test
{
static void F(int x, int y = -1, int z = -2) {
System.Console.WriteLine("x = {0}, y = {1}, z = {2}", x, y, z);
}
genera el resultado
x = 0, y = 1, z = 2
x = 4, y = -1, z = 3
Las reglas de covarianza de matriz (covarianza de matriz) permiten que un valor de un tipo de matriz A[] sea
una referencia a una instancia de un tipo de matriz B[] , siempre que exista una conversión de referencia
implícita de B a A . Debido a estas reglas, cuando un elemento de matriz de un reference_type se pasa como
parámetro de referencia o de salida, se requiere una comprobación en tiempo de ejecución para asegurarse de
que el tipo de elemento real de la matriz es idéntico al del parámetro. En el ejemplo
class Test
{
static void F(ref object x) {...}
F(10, 20);
F(10, 20, 30, 40);
F(10, 20, 1, "hello", 3.0);
En concreto, tenga en cuenta que se crea una matriz vacía cuando no hay ningún argumento dado para la matriz
de parámetros.
Cuando se omiten los argumentos de un miembro de función con los parámetros opcionales correspondientes,
se pasan implícitamente los argumentos predeterminados de la declaración de miembro de función. Dado que
siempre son constantes, su evaluación no afectará al orden de evaluación de los argumentos restantes.
Inferencia de tipos
Cuando se llama a un método genérico sin especificar argumentos de tipo, un proceso de inferencia de tipos
intenta deducir los argumentos de tipo de la llamada. La presencia de la inferencia de tipos permite usar una
sintaxis más cómoda para llamar a un método genérico y permite al programador evitar especificar información
de tipos redundantes. Por ejemplo, dada la declaración del método:
class Chooser
{
static Random rand = new Random();
A través de la inferencia de tipos, los argumentos de tipo int y string se determinan a partir de los
argumentos del método.
La inferencia de tipos se produce como parte del procesamiento en tiempo de enlace de una invocación de
método (invocaciones de método) y tiene lugar antes del paso de resolución de sobrecarga de la invocación.
Cuando se especifica un grupo de métodos determinado en una invocación de método y no se especifica
ningún argumento de tipo como parte de la invocación del método, se aplica la inferencia de tipos a cada
método genérico del grupo de métodos. Si la inferencia de tipos se realiza correctamente, los argumentos de
tipo deducido se usan para determinar los tipos de argumentos para la resolución de sobrecarga subsiguiente.
Si la resolución de sobrecarga elige un método genérico como el que se va a invocar, los argumentos de tipo
deducido se usan como argumentos de tipo reales para la invocación. Si se produce un error en la inferencia de
tipos para un método determinado, ese método no participa en la resolución de sobrecarga. El error de
inferencia de tipos, en y de sí mismo, no produce un error en tiempo de enlace. Sin embargo, a menudo se
produce un error en tiempo de enlace cuando la resolución de sobrecarga no encuentra ningún método
aplicable.
Si el número de argumentos proporcionado es diferente del número de parámetros del método, se producirá
un error inmediatamente en la inferencia. En caso contrario, supongamos que el método genérico tiene la
siguiente firma:
Con una llamada al método de la forma M(E1...Em) , la tarea de inferencia de tipo es buscar argumentos
S1...Sn de tipo únicos para cada uno de los parámetros de tipo X1...Xn , de modo que la llamada
M<S1...Sn>(E1...Em) sea válida.
Durante el proceso de inferencia, cada parámetro de tipo Xi se fija en un tipo determinado Si o se desfija con
un conjunto de límites asociado. Cada uno de los límites es algún tipo T . Inicialmente, cada variable Xi de
tipo se desfija con un conjunto vacío de límites.
La inferencia de tipos tiene lugar en fases. Cada fase intentará deducir los argumentos de tipo para obtener más
variables de tipo basadas en los resultados de la fase anterior. La primera fase realiza algunas inferencias
iniciales de límites, mientras que en la segunda fase se corrigen las variables de tipo a tipos específicos y se
deducen más límites. Es posible que la segunda fase se repita varias veces.
Nota: La inferencia de tipos no solo tiene lugar cuando se llama a un método genérico. La inferencia de tipos
para la conversión de grupos de métodos se describe en inferencia de tipos para la conversión de grupos de
métodos y encontrar el mejor tipo común de un conjunto de expresiones se describe en Buscar el mejor tipo
común de un conjunto de expresiones.
La primera fase
Para cada uno de los argumentos del método Ei :
Si Ei es una función anónima, se realiza una inferencia de tipo de parámetro explícita (inferencias de tipos
de parámetros explícitos) de Ei a Ti
De lo contrario, si Ei tiene un tipo U y xi es un parámetro de valor, se realiza una inferencia de enlace
inferior desde U a Ti .
De lo contrario, si Ei tiene un tipo U y xi es un ref out parámetro o, se realiza una inferencia exacta
desde U a Ti .
De lo contrario, no se realiza ninguna inferencia para este argumento.
La segunda fase
La segunda fase continúa como sigue:
Todas las variables de tipo Xi sin corregir que no dependen de (dependencia) Xj se corrigen (corrigiendo).
Si no existe ninguna variable de tipo, se fijan todas las variables de tipo no fijas Xi para las que se retrasen:
Hay al menos una variable de tipo Xj que depende de Xi
Xi tiene un conjunto de límites no vacío
Si no existe ninguna variable de tipo y hay variables de tipo sin corregir , se produce un error en la inferencia
de tipos.
De lo contrario, si no existen más variables de tipo sin corregir , la inferencia de tipos se realiza
correctamente.
De lo contrario, para todos los argumentos Ei con el tipo de parámetro correspondiente Ti en el que los
tipos de salida (tipos de salida) contienen variables de tipo sin corregir Xj pero los tipos de entrada (tipos
de entrada) no, una inferencia de tipo de salida (inferencias de tipo de salida) se realiza desde Ei a Ti . A
continuación, se repite la segunda fase.
Tipos de entrada
Si E es un grupo de métodos o una función anónima con tipos implícitos y T es un tipo de delegado o un tipo
de árbol de expresión, todos los tipos de parámetro de T son tipos de entrada de tipo E T .
Tipos de salida
Si E es un grupo de métodos o una función anónima y T es un tipo de delegado o un tipo de árbol de
expresión, el tipo de valor devuelto de T es un tipo de salida de E con el tipo T .
Dependencia
Una variable de tipo sin fijo Xi depende directamente de una variable de tipo sin corregir Xj si para algún
argumento Ek con tipo Tk Xj se produce en un tipo de entrada de Ek con el tipo Tk y Xi se produce en
un tipo de salida de Ek con el tipo Tk .
Xj depende de Xi Si Xj depende directamente de Xi o si Xi depende directamente de Xk y Xk depende
de Xj . Por lo tanto, "depende de" es el cierre transitivo pero no reflexivo de "depende directamente de".
Inferencias de tipos de salida
Una inferencia de tipo de salida se realiza desde una expresión E a un tipo T de la siguiente manera:
Si E es una función anónima con el tipo de valor devuelto inferido U (tipo de valordevuelto deducido) y es
un tipo de T delegado o un tipo de árbol de expresión con el tipo de valor devuelto Tb , una inferencia de
límite inferior (inferencias de límite inferior) se realiza desde U a Tb .
De lo contrario, si E es un grupo de métodos y T es un tipo de delegado o de árbol de expresión con tipos
de parámetro T1...Tk y tipo de valor devuelto Tb , y la resolución de sobrecarga de E con los tipos
T1...Tk produce un único método con el tipo de valor devuelto U , se realiza una inferencia de enlace
inferior desde U a Tb .
De lo contrario, si E es una expresión de tipo U , se realiza una inferencia de enlace inferior desde U a T .
De lo contrario, no se realiza ninguna inferencia.
Inferencias explícitas de tipos de parámetros
Una inferencia de tipo de parámetro explícita se realiza desde una expresión E a un tipo T de la siguiente
manera:
Si E es una función anónima con tipo explícito con tipos de parámetro U1...Uk y T es un tipo de delegado
o un tipo de árbol de expresión con tipos de parámetro V1...Vk , para cada Ui se realiza una inferencia
exacta (inferencias exactas) desde Ui hasta el Vi correspondiente
Inferencias exactas
Una inferencia exacta de un tipo U a un tipo V se realiza de la siguiente manera:
Si V es uno de los fijos Xi , U se agrega al conjunto de límites exactos para Xi .
De lo contrario V1...Vk , U1...Uk los conjuntos y se determinan comprobando si se aplica cualquiera de
los casos siguientes:
V es un tipo V1[...] de matriz y U es un tipo U1[...] de matriz del mismo rango
V es el tipo V1? y U es el tipo. U1?
V es un tipo construido C<V1...Vk> y U es un tipo construido. C<U1...Uk>
Si se aplica cualquiera de estos casos, se realiza una inferencia exacta desde cada Ui hasta el
correspondiente Vi .
En caso contrario, no se realiza ninguna inferencia.
Inferencias con límite inferior
Una inferencia de límite inferior desde un tipo U a un tipo V se realiza de la siguiente manera:
Si V es uno de los desfijos Xi , U se agrega al conjunto de límites inferiores para Xi .
De lo contrario, si V es el tipo V1? y U es el tipo U1? , se realiza una inferencia de límite inferior desde
U1 a V1 .
Si se aplica cualquiera de estos casos, se realiza una inferencia desde cada Ui hasta el correspondiente,
como se indica a continuación Vi :
Si Ui no se sabe que es un tipo de referencia, se realiza una inferencia exacta .
De lo contrario, si V es un tipo de matriz, se realiza una inferencia de límite superior .
De lo contrario, si U es C<U1...Uk> , la inferencia depende del parámetro de tipo i-ésima de C :
Si es covariante, se realiza una inferencia enlazada en el límite superior .
Si es contravariante, se realiza una inferencia de límite inferior .
Si es invariable, se realiza una inferencia exacta .
De lo contrario, no se realiza ninguna inferencia.
Corrección de
Una variable de tipo sin corregir Xi con un conjunto de límites se fija de la manera siguiente:
El conjunto de tipos candidatos Uj comienza como el conjunto de todos los tipos del conjunto de límites
para Xi .
A continuación, examinaremos cada límite de a Xi su vez: para cada límite exacto U de Xi todos los tipos
Uj que no sean idénticos a, U se quitan del conjunto de candidatos. Para cada límite inferior U de Xi
todos los tipos Uj a los que no se haya quitado una conversión implícita del U conjunto de candidatos.
Para cada límite superior U de Xi todos los tipos Uj a partir de los cuales no se quita una conversión
implícita de en U el conjunto de candidatos.
Si entre los tipos de candidatos restantes Uj hay un tipo único V desde el que hay una conversión implícita
a todos los demás tipos candidatos, Xi se fija en V .
De lo contrario, se produce un error en la inferencia de tipos.
Tipo de valor devuelto deducido
El tipo de valor devuelto deducido de una función anónima F se usa durante la inferencia de tipos y la
resolución de sobrecarga. El tipo de valor devuelto deducido solo se puede determinar para una función
anónima en la que se conocen todos los tipos de parámetro, ya sea porque se proporcionan explícitamente, se
proporcionan a través de una conversión de función anónima o se infieren durante la inferencia de tipos en una
invocación de método genérico envolvente.
El tipo de resultado deducido se determina de la siguiente manera:
Si el cuerpo de F es una expresión que tiene un tipo, el tipo de resultado deducido de F es el tipo de esa
expresión.
Si el cuerpo de F es un bloque y el conjunto de expresiones de las instrucciones del bloque return tiene un
mejor tipo común T (encontrar el mejor tipo común de un conjunto de expresiones), el tipo de resultado
deducido de F es T .
De lo contrario, no se puede inferir un tipo de resultado para F .
El tipo de valor devuelto deducido se determina de la siguiente manera:
Si F es Async y el cuerpo de F es una expresión clasificada como Nothing (clasificación de expresión) o un
bloque de instrucciones en el que ninguna instrucción return tiene expresiones, el tipo de valor devuelto
deducido es System.Threading.Tasks.Task
Si F es Async y tiene un tipo de resultado deducido T , el tipo de valor devuelto deducido es
System.Threading.Tasks.Task<T> .
Si F no es asincrónico y tiene un tipo de resultado deducido T , el tipo de valor devuelto deducido es T .
De lo contrario, no se puede inferir un tipo de valor devuelto para F .
Como ejemplo de inferencia de tipos que implica funciones anónimas, tenga en cuenta el Select método de
extensión declarado en la System.Linq.Enumerable clase:
namespace System.Linq
{
public static class Enumerable
{
public static IEnumerable<TResult> Select<TSource,TResult>(
this IEnumerable<TSource> source,
Func<TSource,TResult> selector)
{
foreach (TSource element in source) yield return selector(element);
}
}
}
Suponiendo que el System.Linq espacio de nombres se importó con una using cláusula y, dada una clase
Customer con una Name propiedad de tipo string , el Select método se puede usar para seleccionar los
nombres de una lista de clientes:
Dado que los argumentos de tipo no se especificaron explícitamente, se usa la inferencia de tipos para inferir los
argumentos de tipo. En primer lugar, el customers argumento está relacionado con el source parámetro,
infiriendo T a Customer . A continuación, con el proceso de inferencia de tipos de función anónima descrito
anteriormente, c se especifica el tipo Customer y la expresión c.Name está relacionada con el tipo de valor
devuelto del selector parámetro, infiriendo S a string . Por lo tanto, la invocación es equivalente a
y el grupo M de métodos que se asigna al tipo de delegado D la tarea de inferencia de tipo es buscar
argumentos de tipo para S1...Sn que la expresión:
M<S1...Sn>
Para un miembro de función que incluye una matriz de parámetros, si el miembro de función es aplicable a las
reglas anteriores, se dice que es aplicable en su *formulario normal _. Si un miembro de función que incluye
una matriz de parámetros no es aplicable en su forma normal, el miembro de función puede ser aplicable en su
forma expandida _ * * *:
La forma expandida se construye reemplazando la matriz de parámetros en la declaración de miembro de
función con cero o más parámetros de valor del tipo de elemento de la matriz de parámetros de modo que el
número de argumentos de la lista de argumentos A coincida con el número total de parámetros. Si A tiene
menos argumentos que el número de parámetros fijos en la declaración de miembro de función, no se
puede construir la forma expandida del miembro de función y, por tanto, no es aplicable.
De lo contrario, el formulario expandido es aplicable si para cada argumento del A modo de paso de
parámetros del argumento es idéntico al modo de paso de parámetros del parámetro correspondiente, y
para un parámetro de valor fijo o un parámetro de valor creado por la expansión, existe una
conversión implícita (conversiones implícitas) del tipo del argumento al tipo del parámetro
correspondiente, o bien
para un ref out parámetro o, el tipo del argumento es idéntico al tipo del parámetro
correspondiente.
Mejor miembro de función
Con el fin de determinar el mejor miembro de función, se construye una lista de argumentos con la que se ha
quitado una lista que contiene solo las expresiones de argumento en el orden en que aparecen en la lista de
argumentos original.
Las listas de parámetros para cada uno de los miembros de la función candidata se construyen de la siguiente
manera:
La forma expandida se utiliza si el miembro de función solo se aplica en el formulario expandido.
Los parámetros opcionales sin argumentos correspondientes se quitan de la lista de parámetros
Los parámetros se reordenan para que se produzcan en la misma posición que el argumento
correspondiente en la lista de argumentos.
Dada una lista A de argumentos con un conjunto de expresiones de argumentos {E1, E2, ..., En} y dos
miembros de función aplicables Mp y Mq con tipos de parámetros {P1, P2, ..., Pn} y {Q1, Q2, ..., Qn} ,
Mp se define como un miembro de función mejor que Mq si
class G1<U>
{
int F1(U u); // Overload resolution for G<int>.F1
int F1(int i); // will pick non-generic
class G2<U,V>
{
void F3(U u, V v); // Valid, but overload resolution for
void F3(V v, U u); // G2<int,int>.F3 will fail
Expresiones primarias
Las expresiones primarias incluyen las formas más sencillas de las expresiones.
primary_expression
: primary_no_array_creation_expression
| array_creation_expression
;
primary_no_array_creation_expression
: literal
| interpolated_string_expression
| simple_name
| parenthesized_expression
| member_access
| invocation_expression
| element_access
| this_access
| base_access
| post_increment_expression
| post_decrement_expression
| object_creation_expression
| delegate_creation_expression
| anonymous_object_creation_expression
| typeof_expression
| checked_expression
| unchecked_expression
| default_value_expression
| nameof_expression
| anonymous_method_expression
| primary_no_array_creation_expression_unsafe
;
Literales
Un primary_expression que consta de un literal (literales) se clasifica como un valor.
Cadenas interpoladas
Un interpolated_string_expression consta de un $ signo seguido de un literal de cadena normal o textual, en el
que los huecos, delimitados por { y } , escriben expresiones y especificaciones de formato. Una expresión de
cadena interpolada es el resultado de una interpolated_string_literal que se ha dividido en tokens individuales,
tal y como se describe en literales de cadena interpolados.
interpolated_string_expression
: '$' interpolated_regular_string
| '$' interpolated_verbatim_string
;
interpolated_regular_string
: interpolated_regular_string_whole
| interpolated_regular_string_start interpolated_regular_string_body interpolated_regular_string_end
;
interpolated_regular_string_body
: interpolation (interpolated_regular_string_mid interpolation)*
;
interpolation
: expression
| expression ',' constant_expression
;
interpolated_verbatim_string
: interpolated_verbatim_string_whole
| interpolated_verbatim_string_start interpolated_verbatim_string_body interpolated_verbatim_string_end
;
interpolated_verbatim_string_body
: interpolation (interpolated_verbatim_string_mid interpolation)+
;
simple_name
: identifier type_argument_list?
;
parenthesized_expression
: '(' expression ')'
;
predefined_type
: 'bool' | 'byte' | 'char' | 'decimal' | 'double' | 'float' | 'int' | 'long'
| 'object' | 'sbyte' | 'short' | 'string' | 'uint' | 'ulong' | 'ushort'
;
struct Color
{
public static readonly Color White = new Color(...);
public static readonly Color Black = new Color(...);
class A
{
public Color Color; // Field Color of type Color
void F() {
Color = Color.Black; // References Color.Black static member
Color = Color.Complement(); // Invokes Complement() on Color field
}
Ambigüedades de la gramática
Las producciones de simple_name (nombres simples) y member_access (acceso a miembros) pueden dar lugar
a ambigüedades en la gramática de expresiones. Por ejemplo, la instrucción:
F(G<A,B>(7));
podría interpretarse como una llamada a F con dos argumentos, G < A y B > (7) . Como alternativa, podría
interpretarse como una llamada a F con un argumento, que es una llamada a un método genérico G con dos
argumentos de tipo y un argumento normal.
Si se puede analizar una secuencia de tokens (en contexto) como simple_name (nombres simples),
member_access (acceso a miembros) o pointer_member_access (acceso a miembros de puntero) que finaliza
con un type_argument_list (argumentos de tipo), se examina el token inmediatamente posterior al token de
cierre > . Si es uno de
( ) ] } : ; , . ? == != | ^
F(G<A,B>(7));
, según esta regla, se interpretará como una llamada a F con un argumento, que es una llamada a un método
genérico G con dos argumentos de tipo y un argumento normal. Las instrucciones
se interpretará como un operador menor que, mayor que y unario más, como si se hubiera escrito la instrucción
x = (F < A) > (+y) , en lugar de un simple_name con un type_argument_list seguido de un operador binario
Plus. En la instrucción
x = y is C<T> + z;
invocation_expression
: primary_expression '(' argument_list? ')'
;
Un invocation_expression está enlazado dinámicamente (enlace dinámico) si al menos uno de los siguientes
contiene:
El primary_expression tiene el tipo en tiempo de compilación dynamic .
Al menos un argumento del argument_list opcional tiene el tipo en tiempo de compilación dynamic y el
primary_expression no tiene un tipo de delegado.
En este caso, el compilador clasifica el invocation_expression como un valor de tipo dynamic . A continuación, se
aplican las siguientes reglas para determinar el significado de la invocation_expression en tiempo de ejecución,
utilizando el tipo en tiempo de ejecución en lugar del tipo en tiempo de compilación de los primary_expression
y argumentos que tienen el tipo en tiempo de compilación dynamic . Si el primary_expression no tiene el tipo
en tiempo de compilación dynamic , la invocación del método sufre una comprobación limitada del tiempo de
compilación, como se describe en comprobación en tiempo de compilación de la resolución de sobrecarga
dinámica.
El primary_expression de una invocation_expression debe ser un grupo de métodos o un valor de un
delegate_type. Si el primary_expression es un grupo de métodos, el invocation_expression es una invocación de
método (invocaciones de método). Si el primary_expression es un valor de un delegate_type, el
invocation_expression es una invocación de delegado (invocaciones de delegado). Si el primary_expression no
es un grupo de métodos ni un valor de un delegate_type, se produce un error en tiempo de enlace.
El argument_list opcional (listas de argumentos) proporciona valores o referencias a variables para los
parámetros del método.
El resultado de evaluar una invocation_expression se clasifica como sigue:
Si el invocation_expression invoca un método o un delegado que devuelve void , el resultado es Nothing.
Una expresión que se clasifique como Nothing solo se permite en el contexto de una statement_expression
(instrucciones de expresión) o como el cuerpo de un lambda_expression (expresiones de función anónima).
En caso contrario, se produce un error en tiempo de enlace.
De lo contrario, el resultado es un valor del tipo devuelto por el método o el delegado.
Invocaciones de método
En el caso de una invocación de método, el primary_expression de la invocation_expression debe ser un grupo
de métodos. El grupo de métodos identifica el método que se va a invocar o el conjunto de métodos
sobrecargados desde los que se va a elegir un método específico que se va a invocar. En el último caso, la
determinación del método específico que se va a invocar se basa en el contexto proporcionado por los tipos de
los argumentos en el argument_list.
El procesamiento en tiempo de enlace de una invocación de método con el formato M(A) , donde M es un
grupo de métodos (posiblemente incluido un type_argument_list) y A es un argument_list opcional, consta de
los siguientes pasos:
Se construye el conjunto de métodos candidatos para la invocación del método. Para cada método F
asociado al grupo de métodos M :
Si F no es genérico, F es un candidato cuando:
M no tiene ninguna lista de argumentos de tipo y
F es aplicable con respecto a A (miembro de función aplicable).
Si F es genérico y M no tiene ninguna lista de argumentos de tipo, F es un candidato cuando:
La inferencia de tipos (inferencia de tipo) se realiza correctamente, deduciendo una lista de
argumentos de tipo para la llamada y
Una vez que los argumentos de tipo deducido se sustituyen por los parámetros de tipo de
método correspondientes, todos los tipos construidos en la lista de parámetros de F satisfacen
sus restricciones (quecumplencon las restricciones) y la lista de parámetros de F es aplicable
con respecto a A (miembro defunción aplicable).
Si F es genérico e M incluye una lista de argumentos de tipo, F es un candidato cuando:
F tiene el mismo número de parámetros de tipo de método que se proporcionaron en la lista
de argumentos de tipo y
Una vez que los argumentos de tipo se sustituyen por los parámetros de tipo de método
correspondientes, todos los tipos construidos en la lista de parámetros de F satisfacen sus
restricciones (quecumplencon las restricciones) y la lista de parámetros de F es aplicable con
respecto a A (miembro defunción aplicable).
El conjunto de métodos candidatos se reduce para que solo contengan métodos de los tipos más derivados:
para cada método del C.F conjunto, donde C es el tipo en el que F se declara el método, todos los
métodos declarados en un tipo base de C se quitan del conjunto. Además, si C es un tipo de clase distinto
de object , todos los métodos declarados en un tipo de interfaz se quitan del conjunto. (Esta última regla
solo tiene efecto cuando el grupo de métodos era el resultado de una búsqueda de miembros en un
parámetro de tipo que tiene una clase base efectiva que no es un objeto y un conjunto de interfaces efectivo
no vacío).
Si el conjunto resultante de métodos candidatos está vacío, se abandona el procesamiento posterior a lo
largo de los pasos siguientes y, en su lugar, se intenta procesar la invocación como una invocación de
método de extensión (invocaciones de método de extensión). Si se produce un error, no existe ningún
método aplicable y se produce un error en tiempo de enlace.
El mejor método del conjunto de métodos candidatos se identifica mediante las reglas de resolución de
sobrecarga de la resolución de sobrecarga. Si no se puede identificar un único método mejor, la invocación
del método es ambigua y se produce un error en tiempo de enlace. Al realizar la resolución de sobrecarga,
los parámetros de un método genérico se tienen en cuenta después de sustituir los argumentos de tipo
(proporcionados o deducidos) para los parámetros de tipo de método correspondientes.
Se realiza la validación final del mejor método elegido:
El método se valida en el contexto del grupo de métodos: Si el mejor método es un método estático, el
grupo de métodos debe haber sido el resultado de un simple_name o de un member_access a través
de un tipo. Si el mejor método es un método de instancia, el grupo de métodos debe haber sido el
resultado de un simple_name, un member_access a través de una variable o un valor, o un
base_access. Si ninguno de estos requisitos es true, se produce un error en tiempo de enlace.
Si el mejor método es un método genérico, los argumentos de tipo (suministrados o deducidos) se
comprueban con las restricciones (quecumplen las restricciones) declaradas en el método genérico. Si
algún argumento de tipo no satisface las restricciones correspondientes en el parámetro de tipo, se
produce un error en tiempo de enlace.
Una vez que se ha seleccionado y validado un método en tiempo de enlace según los pasos anteriores, la
invocación real en tiempo de ejecución se procesa de acuerdo con las reglas de invocación de miembros de
función descritas en la comprobación en tiempo de compilación de la resolución dinámica de sobrecarga.
El efecto intuitivo de las reglas de resolución descritas anteriormente es el siguiente: para encontrar el método
determinado invocado por una invocación de método, empiece con el tipo indicado por la invocación del
método y continúe con la cadena de herencia hasta que se encuentre al menos una declaración de método
aplicable, accesible y que no sea de invalidación. A continuación, realice la inferencia de tipos y la resolución de
sobrecarga en el conjunto de métodos aplicables, accesibles y no invalidaciones declarados en ese tipo e
invoque el método de la forma que se seleccione. Si no se encuentra ningún método, pruebe en su lugar para
procesar la invocación como una invocación de método de extensión.
Invocaciones de métodos de extensión
En una invocación de método (invocaciones en instancias de conversión boxing) de uno de los formularios
expr . identifier ( )
C . identifier ( expr )
Con C como destino, la llamada al método se procesa a continuación como una invocación de método estático
(comprobación en tiempo de compilación de la resolución dinámica de sobrecarga).
Las reglas anteriores implican que los métodos de instancia tienen prioridad sobre los métodos de extensión,
que los métodos de extensión disponibles en las declaraciones de espacios de nombres internos tienen
prioridad sobre los métodos de extensión disponibles en las declaraciones de espacios de nombres exteriores y
que los métodos de extensión declarados directamente en un espacio de nombres tienen prioridad sobre los
métodos de extensión importados en el mismo espacio de nombres con una directiva Por ejemplo:
class A { }
class B
{
public void F(int i) { }
}
class C
{
public void F(object obj) { }
}
class X
{
static void Test(A a, B b, C c) {
a.F(1); // E.F(object, int)
a.F("hello"); // E.F(object, string)
b.F(1); // B.F(int)
b.F("hello"); // E.F(object, string)
c.F(1); // C.F(object)
c.F("hello"); // C.F(object)
}
}
En el ejemplo, el B método de tiene prioridad sobre el primer método de extensión y el C método de tiene
prioridad sobre ambos métodos de extensión.
public static class C
{
public static void F(this int i) { Console.WriteLine("C.F({0})", i); }
public static void G(this int i) { Console.WriteLine("C.G({0})", i); }
public static void H(this int i) { Console.WriteLine("C.H({0})", i); }
}
namespace N1
{
public static class D
{
public static void F(this int i) { Console.WriteLine("D.F({0})", i); }
public static void G(this int i) { Console.WriteLine("D.G({0})", i); }
}
}
namespace N2
{
using N1;
class Test
{
static void Main(string[] args)
{
1.F();
2.G();
3.H();
}
}
}
E.F(1)
D.G(2)
C.H(3)
D.G tiene prioridad sobre y C.G E.F tiene prioridad sobre D.F y C.F .
Invocaciones de delegado
En el caso de una invocación de delegado, el primary_expression de la invocation_expression debe ser un valor
de un delegate_type. Además, si se considera que el delegate_type ser un miembro de función con la misma
lista de parámetros que el delegate_type, el delegate_type debe ser aplicable (miembro de función aplicable) con
respecto al argument_list del invocation_expression.
El procesamiento en tiempo de ejecución de una invocación de delegado con el formato D(A) , donde D es
una primary_expression de un delegate_type y A es un argument_list opcional, consta de los siguientes pasos:
Dse evalúa. Si esta evaluación provoca una excepción, no se ejecuta ningún paso más.
D Se comprueba que el valor de es válido. Si el valor de D es null , System.NullReferenceException se
produce una excepción y no se ejecuta ningún paso más.
De lo contrario, D es una referencia a una instancia de delegado. Las invocaciones de miembros de función
(comprobación en tiempo de compilación de la resolución dinámica de sobrecarga) se realizan en cada una
de las entidades a las que se puede llamar en la lista de invocaciones del delegado. Para las entidades a las
que se puede llamar que se componen de un método de instancia y de instancia, la instancia de para la
invocación es la instancia contenida en la entidad a la que se puede
Acceso a elementos
Un element_access consta de un primary_no_array_creation_expression, seguido de un [ token "", seguido de
un argument_list, seguido de un ] token "". El argument_list se compone de uno o más argumentos, separados
por comas.
element_access
: primary_no_array_creation_expression '[' expression_list ']'
;
this_access
: 'this'
;
base_access
: 'base' '.' identifier
| 'base' '[' expression_list ']'
;
Un base_access se utiliza para tener acceso a los miembros de clase base que están ocultos por miembros con el
mismo nombre en la clase o el struct actual. Solo se permite un base_access en el bloque de un constructor de
instancia, un método de instancia o un descriptor de acceso de instancia. Cuando base.I se produce en una
clase o struct, I debe indicar un miembro de la clase base de esa clase o estructura. Del mismo modo, cuando
base[E] se produce en una clase, debe existir un indexador aplicable en la clase base.
En tiempo de enlace, base_access expresiones del formulario base.I y base[E] se evalúan exactamente como
si se hubieran escrito ((B)this).I y ((B)this)[E] , donde B es la clase base de la clase o estructura en la que
se produce la construcción. Así, base.I y base[E] corresponden a this.I y this[E] , salvo this que se ve
como una instancia de la clase base.
Cuando un base_access hace referencia a un miembro de función virtual (un método, una propiedad o un
indizador), se cambia la determinación del miembro de función que se va a invocar en tiempo de ejecución
(comprobación en tiempo de compilación de la resolución dinámica de sobrecarga). El miembro de función que
se invoca se determina mediante la búsqueda de la implementación más derivada (métodos virtuales) del
miembro de función con respecto a B (en lugar de con respecto al tipo en tiempo de ejecución de this , como
sería habitual en un acceso no base). Por lo tanto, dentro override de un virtual miembro de función, se
puede usar un base_access para invocar la implementación heredada del miembro de función. Si el miembro de
función al que hace referencia una base_access es abstracto, se produce un error en tiempo de enlace.
Operadores de incremento y decremento posfijos
post_increment_expression
: primary_expression '++'
;
post_decrement_expression
: primary_expression '--'
;
El operando de una operación de incremento o decremento postfijo debe ser una expresión clasificada como
una variable, un acceso de propiedad o un acceso de indexador. El resultado de la operación es un valor del
mismo tipo que el operando.
Si el primary_expression tiene el tipo en tiempo de compilación dynamic , el operador está enlazado
dinámicamente (enlace dinámico), el post_increment_expression o post_decrement_expression tiene el tipo en
tiempo de compilación dynamic y se aplican las siguientes reglas en tiempo de ejecución utilizando el tipo en
tiempo de ejecución del primary_expression.
Si el operando de una operación de incremento o decremento postfijo es una propiedad o un indexador, la
propiedad o el indexador deben tener un get set descriptor de acceso y. Si no es así, se produce un error en
tiempo de enlace.
La resolución de sobrecargas de operador unario (resolución de sobrecarga de operadores unarios) se aplica
para seleccionar una implementación de operador específica. ++ Existen operadores y predefinidos -- para los
tipos siguientes: sbyte , byte , short , ushort , int , uint ,,, long ulong char , float , double ,
decimal y cualquier tipo de enumeración. Los operadores predefinidos ++ devuelven el valor generado
agregando 1 al operando y los operadores predefinidos -- devuelven el valor generado restando 1 del
operando. En un checked contexto, si el resultado de esta suma o resta está fuera del intervalo del tipo de
resultado y el tipo de resultado es un tipo entero o de enumeración, System.OverflowException se produce una
excepción.
El procesamiento en tiempo de ejecución de una operación de incremento o decremento postfijo del formulario
x++ o x-- consta de los siguientes pasos:
object_creation_expression
: 'new' type '(' argument_list? ')' object_or_collection_initializer?
| 'new' type object_or_collection_initializer
;
object_or_collection_initializer
: object_initializer
| collection_initializer
;
object_initializer
: '{' member_initializer_list? '}'
| '{' member_initializer_list ',' '}'
;
member_initializer_list
: member_initializer (',' member_initializer)*
;
member_initializer
: initializer_target '=' initializer_value
;
initializer_target
: identifier
| '[' argument_list ']'
;
initializer_value
: expression
| object_or_collection_initializer
;
Un inicializador de objeto consta de una secuencia de inicializadores de miembro, delimitada por { } tokens y
y separados por comas. Cada member_initializer designa un destino para la inicialización. Un identificador debe
asignar un nombre a un campo o propiedad accesible del objeto que se va a inicializar, mientras que una
argument_list entre corchetes debe especificar los argumentos de un indizador accesible en el objeto que se va
a inicializar. Es un error que un inicializador de objeto incluya más de un inicializador de miembro para el mismo
campo o propiedad.
Cada initializer_target va seguido de un signo igual y una expresión, un inicializador de objeto o un inicializador
de colección. No es posible que las expresiones del inicializador de objeto hagan referencia al objeto recién
creado que se está inicializando.
Inicializador de miembro que especifica una expresión después de que el signo igual se procese de la misma
manera que una asignación (asignación simple) al destino.
Inicializador de miembro que especifica un inicializador de objeto después de que el signo igual sea un
inicializador de objeto anidado , es decir, una inicialización de un objeto incrustado. En lugar de asignar un
nuevo valor al campo o propiedad, las asignaciones en el inicializador de objeto anidado se tratan como
asignaciones a los miembros del campo o de la propiedad. Los inicializadores de objeto anidados no se pueden
aplicar a las propiedades con un tipo de valor o a los campos de solo lectura con un tipo de valor.
Inicializador de miembro que especifica un inicializador de colección después de que el signo igual sea una
inicialización de una colección incrustada. En lugar de asignar una nueva colección al campo de destino,
propiedad o indizador, los elementos proporcionados en el inicializador se agregan a la colección a la que hace
referencia el destino. El destino debe ser de un tipo de colección que satisfaga los requisitos especificados en los
inicializadores de colección.
Los argumentos de un inicializador de índice siempre se evaluarán exactamente una vez. Por lo tanto, aunque
los argumentos acaben nunca en usarse (por ejemplo, debido a un inicializador anidado vacío), se evaluarán por
sus efectos secundarios.
La clase siguiente representa un punto con dos coordenadas:
public class Point
{
int x, y;
donde __a es una variable temporal invisible e inaccesible en caso contrario. La clase siguiente representa un
rectángulo creado a partir de dos puntos:
donde __r __p1 y __p2 son variables temporales que, de lo contrario, son invisibles e inaccesibles.
Rectangle El constructor de si asigna las dos instancias incrustadas Point
public class Rectangle
{
Point p1 = new Point();
Point p2 = new Point();
la construcción siguiente se puede utilizar para inicializar las Point instancias incrustadas en lugar de asignar
nuevas instancias:
var c = new C {
x = true,
y = { a = "Hello" },
z = { 1, 2, 3 },
["x"] = 5,
[0,0] = { "a", "b" },
[1,2] = {}
};
donde __c , etc., se generan variables que son invisibles e inaccesibles para el código fuente. Tenga en cuenta
que los argumentos de [0,0] se evalúan solo una vez, y los argumentos de [1,2] se evalúan una vez, aunque
nunca se utilicen.
Inicializadores de colección
Un inicializador de colección especifica los elementos de una colección.
collection_initializer
: '{' element_initializer_list '}'
| '{' element_initializer_list ',' '}'
;
element_initializer_list
: element_initializer (',' element_initializer)*
;
element_initializer
: non_assignment_expression
| '{' expression_list '}'
;
expression_list
: expression (',' expression)*
;
El objeto de colección al que se aplica un inicializador de colección debe ser de un tipo que implemente
System.Collections.IEnumerable o se produzca un error en tiempo de compilación. Para cada elemento
especificado en orden, el inicializador de colección invoca un Add método en el objeto de destino con la lista de
expresiones del inicializador de elemento como lista de argumentos, aplicando la búsqueda de miembros
normal y la resolución de sobrecarga para cada invocación. Por lo tanto, el objeto de colección debe tener una
instancia o un método de extensión aplicable con el nombre Add de cada inicializador de elemento.
La clase siguiente representa un contacto con un nombre y una lista de números de teléfono:
donde __clist __c1 y __c2 son variables temporales que, de lo contrario, son invisibles e inaccesibles.
Expresiones de creación de matrices
Un array_creation_expression se usa para crear una nueva instancia de un array_type.
array_creation_expression
: 'new' non_array_type '[' expression_list ']' rank_specifier* array_initializer?
| 'new' array_type array_initializer
| 'new' rank_specifier array_initializer
;
Una expresión de creación de matriz del primer formulario asigna una instancia de matriz del tipo resultante de
la eliminación de cada una de las expresiones individuales de la lista de expresiones. Por ejemplo, la expresión
de creación de matriz new int[10,20] genera una instancia de matriz de tipo int[,] y la expresión
new int[10][,] de creación de matriz genera una matriz de tipo int[][,] . Cada expresión de la lista de
expresiones debe ser de tipo int , uint , long o ulong , o bien se puede convertir implícitamente a uno o
varios de estos tipos. El valor de cada expresión determina la longitud de la dimensión correspondiente en la
instancia de matriz recién asignada. Dado que la longitud de una dimensión de matriz no debe ser negativa, es
un error en tiempo de compilación tener un constant_expression con un valor negativo en la lista de
expresiones.
Excepto en un contexto no seguro (contextos no seguros), no se especifica el diseño de las matrices.
Si una expresión de creación de matriz del primer formulario incluye un inicializador de matriz, cada expresión
de la lista de expresiones debe ser una constante y las longitudes de rango y dimensión especificadas por la lista
de expresiones deben coincidir con las del inicializador de matriz.
En una expresión de creación de matriz del segundo o tercer formulario, el rango del tipo de matriz o
especificador de rango especificado debe coincidir con el del inicializador de matriz. Las longitudes de las
dimensiones individuales se deducen del número de elementos de cada uno de los niveles de anidamiento
correspondientes del inicializador de matriz. Por lo tanto, la expresión
new int[,] {{0, 1}, {2, 3}, {4, 5}}
corresponde exactamente a
Se hace referencia a una expresión de creación de matriz del tercer formulario como una expresión de
creación de matriz con tipo * implícita _. Es similar a la segunda forma, salvo que el tipo de elemento de la
matriz no se proporciona explícitamente, pero se determina como el mejor tipo común (encontrar el mejor tipo
común de un conjunto de expresiones) del conjunto de expresiones en el inicializador de matriz. En el caso de
una matriz multidimensional, es decir, una en la que el _rank_specifier * contiene al menos una coma, este
conjunto incluye todas las expresiones que se encuentran en los array_initializer anidados.
Los inicializadores de matriz se describen con más detalle en inicializadores de matriz.
El resultado de evaluar una expresión de creación de matriz se clasifica como un valor, es decir, una referencia a
la instancia de la matriz recién asignada. El procesamiento en tiempo de ejecución de una expresión de creación
de matriz consta de los siguientes pasos:
Las expresiones de longitud de dimensión del expression_list se evalúan en orden, de izquierda a derecha.
Después de la evaluación de cada expresión, se realiza una conversión implícita (conversiones implícitas) en
uno de los siguientes tipos int : uint ,, long , ulong . Se elige el primer tipo de esta lista para el que
existe una conversión implícita. Si la evaluación de una expresión o la conversión implícita subsiguiente
produce una excepción, no se evalúan más expresiones y no se ejecuta ningún otro paso.
Los valores calculados de las longitudes de dimensión se validan como se indica a continuación. Si uno o
varios de los valores son menores que cero, System.OverflowException se produce una excepción y no se
ejecuta ningún otro paso.
Se asigna una instancia de matriz con las longitudes de dimensión dadas. Si no hay suficiente memoria
disponible para asignar la nueva instancia, System.OutOfMemoryException se produce una excepción y no se
ejecuta ningún paso más.
Todos los elementos de la nueva instancia de matriz se inicializan con sus valores predeterminados (valores
predeterminados).
Si la expresión de creación de matriz contiene un inicializador de matriz, cada expresión del inicializador de
matriz se evalúa y se asigna a su elemento de matriz correspondiente. Las evaluaciones y las asignaciones se
realizan en el orden en el que se escriben las expresiones en el inicializador de matriz; es decir, los elementos
se inicializan en el orden de índice creciente, con la dimensión situada más a la derecha aumentando
primero. Si la evaluación de una expresión determinada o la asignación subsiguiente al elemento de matriz
correspondiente produce una excepción, no se inicializa ningún otro elemento (y los demás elementos
tendrán sus valores predeterminados).
Una expresión de creación de matriz permite la creación de instancias de una matriz con elementos de un tipo
de matriz, pero los elementos de dicha matriz se deben inicializar manualmente. Por ejemplo, la instrucción
crea una matriz unidimensional con 100 elementos de tipo int[] . El valor inicial de cada elemento es null .
No es posible que la misma expresión de creación de matriz cree también instancias de las submatrices y la
instrucción
Cuando una matriz de matrices tiene una forma "rectangular", es decir, cuando las submatrices tienen la misma
longitud, es más eficaz usar una matriz multidimensional. En el ejemplo anterior, la creación de instancias de la
matriz de matrices crea 101 objetos, una matriz externa y submatrices de 100. En cambio,
crea un solo objeto, una matriz bidimensional y realiza la asignación en una única instrucción.
A continuación se muestran ejemplos de expresiones de creación de matrices con tipo implícito:
La última expresión genera un error en tiempo de compilación porque ni int ni string se pueden convertir
implícitamente al otro, por lo que no hay ningún tipo común más adecuado. En este caso, se debe usar una
expresión de creación de matriz con tipo explícito, por ejemplo, especificando el tipo object[] . Como
alternativa, uno de los elementos se puede convertir a un tipo base común, que luego se convertiría en el tipo
de elemento deducido.
Las expresiones de creación de matrices con tipo implícito se pueden combinar con inicializadores de objeto
anónimos (expresiones de creación de objetos anónimos) para crear estructuras de datos con tipos anónimos.
Por ejemplo:
delegate_creation_expression
: 'new' delegate_type '(' expression ')'
;
El argumento de una expresión de creación de delegado debe ser un grupo de métodos, una función anónima o
un valor del tipo en tiempo de compilación dynamic o un delegate_type. Si el argumento es un grupo de
métodos, identifica el método y, para un método de instancia, el objeto para el que se va a crear un delegado. Si
el argumento es una función anónima, define directamente los parámetros y el cuerpo del método del destino
del delegado. Si el argumento es un valor, identifica una instancia de delegado de la que se va a crear una copia.
Si la expresión tiene el tipo en tiempo de compilación dynamic , el delegate_creation_expression está enlazado
dinámicamente (enlace dinámico) y las reglas siguientes se aplican en tiempo de ejecución mediante el tipo en
tiempo de ejecución de la expresión. De lo contrario, las reglas se aplican en tiempo de compilación.
El procesamiento en tiempo de enlace de un delegate_creation_expression del formulario new D(E) , donde D
es una delegate_type y E es una expresión, consta de los siguientes pasos:
Si E es un grupo de métodos, la expresión de creación de delegado se procesa de la misma manera que una
conversión de grupo de métodos (conversiones de grupo de métodos) de E a D .
Si E es una función anónima, la expresión de creación de delegado se procesa de la misma manera que una
conversión de función anónima (conversiones de funciones anónimas) de E a D .
Si E es un valor, E debe ser compatible (declaraciones de delegado) con y D el resultado es una referencia
a un delegado recién creado de tipo D que hace referencia a la misma lista de invocación que E . Si E no
es compatible con D , se produce un error en tiempo de compilación.
La lista de invocaciones de un delegado se determina cuando se crea una instancia del delegado y permanece
constante durante toda la duración del delegado. En otras palabras, no es posible cambiar las entidades a las
que se puede llamar de destino de un delegado una vez que se ha creado. Cuando se combinan dos delegados o
uno se quita de otro (declaraciones de delegado), se genera un nuevo delegado. no se ha cambiado el contenido
de ningún delegado existente.
No es posible crear un delegado que haga referencia a una propiedad, un indexador, un operador definido por el
usuario, un constructor de instancia, un destructor o un constructor estático.
Como se describió anteriormente, cuando se crea un delegado a partir de un grupo de métodos, la lista de
parámetros formales y el tipo de valor devuelto del delegado determinan cuál de los métodos sobrecargados se
deben seleccionar. En el ejemplo
delegate double DoubleFunc(double x);
class A
{
DoubleFunc f = new DoubleFunc(Square);
el A.f campo se inicializa con un delegado que hace referencia al segundo Square método porque ese método
coincide exactamente con la lista de parámetros formales y el tipo de valor devuelto de DoubleFunc . Si el
segundo Square método no estuviera presente, se habría producido un error en tiempo de compilación.
Expresiones de creación de objetos anónimos
Un anonymous_object_creation_expression se usa para crear un objeto de un tipo anónimo.
anonymous_object_creation_expression
: 'new' anonymous_object_initializer
;
anonymous_object_initializer
: '{' member_declarator_list? '}'
| '{' member_declarator_list ',' '}'
;
member_declarator_list
: member_declarator (',' member_declarator)*
;
member_declarator
: simple_name
| member_access
| base_access
| null_conditional_member_access
| identifier '=' expression
;
Un inicializador de objeto anónimo declara un tipo anónimo y devuelve una instancia de ese tipo. Un tipo
anónimo es un tipo de clase sin nombre que hereda directamente de object . Los miembros de un tipo
anónimo son una secuencia de propiedades de solo lectura que se deducen del inicializador de objeto anónimo
utilizado para crear una instancia del tipo. En concreto, un inicializador de objeto anónimo con el formato
la asignación en la última línea se permite porque p1 y p2 son del mismo tipo anónimo.
Los Equals GetHashcode métodos y en los tipos anónimos invalidan los métodos heredados de y object se
definen en términos de Equals y de las GetHashcode propiedades, de modo que dos instancias del mismo tipo
anónimo son iguales si y solo si todas sus propiedades son iguales.
Un declarador de miembro se puede abreviar como un nombre simple (inferencia de tipos), un acceso a
miembro (comprobación en tiempo de compilación de la resolución dinámica de sobrecarga), un acceso base
(acceso base) o un acceso a miembro condicional nulo (expresiones condicionales NULL como inicializadores de
proyección). Esto se denomina inicializador de proyección y es la abreviatura de una declaración de y la
asignación a una propiedad con el mismo nombre. En concreto, los declaradores de miembros de los
formularios
identifier
expr.identifier
Por lo tanto, en un inicializador de proyección, el identificador selecciona tanto el valor como el campo o la
propiedad a los que se asigna el valor. De manera intuitiva, un inicializador de proyección proyecta no solo un
valor, sino también el nombre del valor.
El operador typeof
El typeof operador se usa para obtener el System.Type objeto para un tipo.
typeof_expression
: 'typeof' '(' type ')'
| 'typeof' '(' unbound_type_name ')'
| 'typeof' '(' 'void' ')'
;
unbound_type_name
: identifier generic_dimension_specifier?
| identifier '::' identifier generic_dimension_specifier?
| unbound_type_name '.' identifier generic_dimension_specifier?
;
generic_dimension_specifier
: '<' comma* '>'
;
comma
: ','
;
La primera forma de typeof_expression consta de una typeof palabra clave seguida de un tipo entre paréntesis.
El resultado de una expresión de este formulario es el System.Type objeto para el tipo indicado. Solo hay un
System.Type objeto para un tipo determinado. Esto significa que para un tipo T , typeof(T) == typeof(T)
siempre es true. El tipo no puede ser dynamic .
La segunda forma de typeof_expression consta de una typeof palabra clave seguida de un
unbound_type_name entre paréntesis. Un unbound_type_name es muy similar a un type_name (espacio de
nombres y nombres de tipo), salvo que un unbound_type_name contiene generic_dimension_specifier s donde
una type_name contiene type_argument_list s. Cuando el operando de una typeof_expression es una secuencia
de tokens que satisface las gramáticas de unbound_type_name y type_name, es decir, cuando no contiene un
generic_dimension_specifier ni un type_argument_list, la secuencia de tokens se considera una type_name. El
significado de un unbound_type_name se determina de la manera siguiente:
Convierta la secuencia de tokens en un type_name reemplazando cada generic_dimension_specifier por un
type_argument_list que tenga el mismo número de comas y la palabra clave object que cada
type_argument.
Evalúe el type_name resultante, pasando por alto todas las restricciones de parámetros de tipo.
El unbound_type_name se resuelve en el tipo genérico sin enlazar asociado con el tipo construido resultante
(tipos enlazados y sin enlazar).
El resultado de la typeof_expression es el System.Type objeto para el tipo genérico sin enlazar resultante.
La tercera forma de typeof_expression consta de una typeof palabra clave seguida de una void palabra clave
entre paréntesis. El resultado de una expresión de este formulario es el System.Type objeto que representa la
ausencia de un tipo. El objeto de tipo devuelto por typeof(void) es distinto del objeto de tipo devuelto para
cualquier tipo. Este objeto de tipo especial es útil en las bibliotecas de clases que permiten la reflexión en
métodos del lenguaje, donde esos métodos desean tener una manera de representar el tipo de valor devuelto
de cualquier método, incluidos los métodos void, con una instancia de System.Type .
El typeof operador se puede utilizar en un parámetro de tipo. El resultado es el System.Type objeto para el tipo
en tiempo de ejecución que se ha enlazado al parámetro de tipo. El typeof operador también se puede usar en
un tipo construido o en un tipo genérico sin enlazar (tipos enlazados y sin enlazar). El System.Type objeto para
un tipo genérico sin enlazar no es el mismo que el System.Type objeto del tipo de instancia. El tipo de instancia
es siempre un tipo construido cerrado en tiempo de ejecución, por lo que su System.Type objeto depende de los
argumentos de tipo en tiempo de ejecución en uso, mientras que el tipo genérico sin enlazar no tiene ningún
argumento de tipo.
En el ejemplo
using System;
class X<T>
{
public static void PrintTypes() {
Type[] t = {
typeof(int),
typeof(System.Int32),
typeof(string),
typeof(double[]),
typeof(void),
typeof(T),
typeof(X<T>),
typeof(X<X<T>>),
typeof(X<>)
};
for (int i = 0; i < t.Length; i++) {
Console.WriteLine(t[i]);
}
}
}
class Test
{
static void Main() {
X<int>.PrintTypes();
}
}
System.Int32
System.Int32
System.String
System.Double[]
System.Void
System.Int32
X`1[System.Int32]
X`1[X`1[System.Int32]]
X`1[T]
checked_expression
: 'checked' '(' expression ')'
;
unchecked_expression
: 'unchecked' '(' expression ')'
;
Las siguientes operaciones se ven afectadas por el contexto de comprobación de desbordamiento establecido
por los checked unchecked operadores y y las instrucciones:
Los operadores predefinidos ++ y -- unarios (operadores deincremento y decremento postfijo y
operadores de incremento y decremento de prefijo) cuando el operando es de un tipo entero.
- Operador unario predefinido (operador unario menos), cuando el operando es de un tipo entero.
Los + operadores binarios predefinidos,, - * y / (operadores aritméticos), cuando ambos operandos
son de tipos enteros.
Conversiones numéricas explícitas (Conversiones numéricas explícitas) de un tipo entero a otro tipo entero, o
de float o double a un tipo entero.
Cuando una de las operaciones anteriores produce un resultado que es demasiado grande para representarlo
en el tipo de destino, el contexto en el que se realiza la operación controla el comportamiento resultante:
En un checked contexto, si la operación es una expresión constante (expresiones constantes), se produce un
error en tiempo de compilación. De lo contrario, cuando la operación se realiza en tiempo de ejecución,
System.OverflowException se produce una excepción.
En un unchecked contexto, el resultado se trunca descartando los bits de orden superior que no caben en el
tipo de destino.
En el caso de las expresiones que no son constantes (expresiones que se evalúan en tiempo de ejecución) que
no están incluidas en ningún checked unchecked operador o instrucción, el contexto de comprobación de
desbordamiento predeterminado es unchecked a menos que los factores externos (como los modificadores de
compilador y la configuración del entorno de ejecución) llamen a para su checked evaluación.
En el caso de expresiones constantes (expresiones que se pueden evaluar por completo en tiempo de
compilación), el contexto de comprobación de desbordamiento predeterminado siempre es checked . A menos
que una expresión constante se coloque explícitamente en un unchecked contexto, los desbordamientos que se
producen durante la evaluación en tiempo de compilación de la expresión siempre producen errores en tiempo
de compilación.
El cuerpo de una función anónima no se ve afectado por checked o unchecked los contextos en los que se
produce la función anónima.
En el ejemplo
class Test
{
static readonly int x = 1000000;
static readonly int y = 1000000;
no se detectan errores en tiempo de compilación, ya que ninguna de las expresiones se puede evaluar en
tiempo de compilación. En tiempo de ejecución, el F método produce una excepción System.OverflowException
y el G método devuelve-727379968 (los 32 bits inferiores del resultado fuera del intervalo). El
comportamiento del H método depende del contexto de comprobación de desbordamiento predeterminado
para la compilación, pero es igual o igual que F G .
En el ejemplo
class Test
{
const int x = 1000000;
const int y = 1000000;
los desbordamientos que se producen al evaluar las expresiones constantes en F y H provocan que se
notifiquen los errores en tiempo de compilación porque las expresiones se evalúan en un checked contexto.
También se produce un desbordamiento al evaluar la expresión constante en G , pero como la evaluación tiene
lugar en un unchecked contexto, el desbordamiento no se registra.
Los checked unchecked operadores y solo afectan al contexto de comprobación de desbordamiento para las
operaciones que están contenidas textualmente en los ( tokens "" y "" ) . Los operadores no tienen ningún
efecto en los miembros de función que se invocan como resultado de evaluar la expresión contenida. En el
ejemplo
class Test
{
static int Multiply(int x, int y) {
return x * y;
}
class Test
{
public const int AllBits = unchecked((int)0xFFFFFFFF);
Las dos constantes hexadecimales anteriores son de tipo uint . Dado que las constantes están fuera del int
intervalo, sin el unchecked operador, las conversiones a int generarán errores en tiempo de compilación.
Los checked unchecked operadores y y las instrucciones permiten a los programadores controlar determinados
aspectos de algunos cálculos numéricos. Sin embargo, el comportamiento de algunos operadores numéricos
depende de los tipos de datos de los operandos. Por ejemplo, si se multiplican dos decimales, siempre se
produce una excepción en el desbordamiento incluso dentro de una unchecked construcción explícita. Del
mismo modo, si se multiplican dos floats, nunca se produce una excepción en el desbordamiento incluso dentro
de una checked construcción explícita. Además, otros operadores nunca se ven afectados por el modo de
comprobación, ya sea de forma predeterminada o explícita.
Expresiones de valor predeterminado
Una expresión de valor predeterminado se usa para obtener el valor predeterminado (valores predeterminados)
de un tipo. Normalmente, se usa una expresión de valor predeterminado para los parámetros de tipo, ya que es
posible que no se conozca si el parámetro de tipo es un tipo de valor o un tipo de referencia. (No existe ninguna
conversión del null literal a un parámetro de tipo a menos que se sepa que el parámetro de tipo es un tipo de
referencia).
default_value_expression
: 'default' '(' type ')'
;
nameof_expression
: 'nameof' '(' named_entity ')'
;
named_entity
: simple_name
| named_entity_target '.' identifier type_argument_list?
;
named_entity_target
: 'this'
| 'base'
| named_entity
| predefined_type
| qualified_alias_member
;
Gramaticalmente hablando, el operando named_entity siempre es una expresión. Dado que nameof no es una
palabra clave reservada, una expresión name es siempre sintácticamente ambigua con una invocación del
nombre simple nameof . Por motivos de compatibilidad, si la búsqueda de nombres (nombres simples) del
nombre nameof se realiza correctamente, la expresión se trata como un invocation_expression ,
independientemente de si la invocación es legal. En caso contrario, es un nameof_expression.
El significado de la named_entity de una nameof_expression es el significado de la misma como una expresión;
es decir, como un simple_name, un base_access o un member_access. Sin embargo, si la búsqueda descrita en
nombres simples y acceso a miembros produce un error porque se encontró un miembro de instancia en un
contexto estático, un nameof_expression no genera este error.
Se trata de un error en tiempo de compilación para un named_entity que designa que un grupo de métodos
tiene un type_argument_list. Es un error en tiempo de compilación para que un named_entity_target tenga el
tipo dynamic .
Un nameof_expression es una expresión constante de tipo string y no tiene ningún efecto en tiempo de
ejecución. En concreto, su named_entity no se evalúa y se omite para los fines del análisis de asignación
definitiva (reglas generales para expresiones simples). Su valor es el último identificador de la named_entity
antes del type_argument_list final opcional, transformada de la siguiente manera:
El prefijo " @ ", si se utiliza, se quita.
Cada unicode_escape_sequence se transforma en el carácter Unicode correspondiente.
Se quitan los formatting_characters .
Se trata de las mismas transformaciones aplicadas en los identificadores al probar la igualdad entre los
identificadores.
TODO: ejemplos
Expresiones de método anónimo
Una anonymous_method_expression es una de las dos formas de definir una función anónima. Se describen
con más detalle en expresiones de función anónima.
Operadores unarios
Los ? + operadores,, - , ! , ~ , ++ , -- , CAST y await se denominan operadores unarios.
unary_expression
: primary_expression
| null_conditional_expression
| '+' unary_expression
| '-' unary_expression
| '!' unary_expression
| '~' unary_expression
| pre_increment_expression
| pre_decrement_expression
| cast_expression
| await_expression
| unary_expression_unsafe
;
null_conditional_expression
: primary_expression null_conditional_operations
;
null_conditional_operations
: null_conditional_operations? '?' '.' identifier type_argument_list?
| null_conditional_operations? '?' '[' argument_list ']'
| null_conditional_operations '.' identifier type_argument_list?
| null_conditional_operations '[' argument_list ']'
| null_conditional_operations '(' argument_list? ')'
;
La lista de operaciones puede incluir operaciones de acceso a miembros y acceso a elementos (que pueden ser a
su vez valores condicionales null), así como invocación.
Por ejemplo, la expresión a.b?[0]?.c() es un null_conditional_expression con un primary_expression a.b y
null_conditional_operations ?[0] (acceso a un elemento condicional null), ?.c (acceso a miembro condicional
null) y () (Invocación).
Para un null_conditional_expression E con un primary_expression P , supongamos que E0 es la expresión
obtenida quitando textualmente el interlineado ? de cada una de las null_conditional_operations de E que
tienen uno. Conceptualmente, E0 es la expresión que se evaluará si ninguna de las comprobaciones nulas
representadas por ? s encuentra un null .
Además, supongamos que E1 es la expresión obtenida quitando textualmente el principio de ? solo el
primero de la null_conditional_operations en E . Esto puede conducir a una expresión principal (si solo había
una ? ) o a otra null_conditional_expression.
Por ejemplo, si E es la expresión a.b?[0]?.c() , E0 es la expresión a.b[0].c() y E1 es la expresión
a.b[0]?.c() .
Si E0 se clasifica como Nothing, E se clasifica como Nothing. De lo contrario, E se clasifica como un valor.
E0 y E1 se usan para determinar el significado de E :
Si E se produce como statement_expression el significado de E es el mismo que el de la instrucción
Si E1 es un null_conditional_expression, estas reglas se aplican de nuevo, anidando las pruebas para null
hasta que no haya más ? , y la expresión se ha reducido hasta el final de la expresión principal E0 .
Por ejemplo, si la expresión a.b?[0]?.c() se produce como una expresión de instrucción, como en la
instrucción:
a.b?[0]?.c();
su significado es equivalente a:
lo que es equivalente a:
var x = a.b?[0]?.c();
y suponiendo que el tipo de la invocación final no es un tipo de valor que no acepta valores NULL, su significado
es equivalente a:
var x = (a.b == null) ? null : (a.b[0] == null) ? null : a.b[0].c();
null_conditional_member_access
: primary_expression null_conditional_operations? '?' '.' identifier type_argument_list?
| primary_expression null_conditional_operations '.' identifier type_argument_list?
;
null_conditional_invocation_expression
: primary_expression null_conditional_operations '(' argument_list? ')'
;
Para cada uno de estos operadores, el resultado es simplemente el valor del operando.
Operador unario menos
En el caso de una operación -x con el formato, se aplica la resolución de sobrecargas de operador unario
(resolución de sobrecarga del operador unario) para seleccionar una implementación de operador específica. El
operando se convierte al tipo de parámetro del operador seleccionado y el tipo del resultado es el tipo de valor
devuelto del operador. Los operadores de negación predefinidos son:
Negación de entero:
int operator -(int x);
long operator -(long x);
El resultado se calcula restando x de cero. Si el valor de x es el menor valor representable del tipo de
operando (-2 ^ 31 para int o-2 ^ 63 para long ), la negación matemática de x no se podrá
representar en el tipo de operando. Si esto ocurre dentro de un checked contexto,
System.OverflowException se produce una excepción; si se produce dentro de un unchecked contexto, el
resultado es el valor del operando y no se registra el desbordamiento.
Si el operando del operador de negación es de tipo uint , se convierte al tipo long y el tipo del
resultado es long . Una excepción es la regla que permite int escribir el valor-2147483648 (-2 ^ 31)
como un literal entero decimal (literales enteros).
Si el operando del operador de negación es de tipo ulong , se produce un error en tiempo de
compilación. Una excepción es la regla que permite long escribir el valor-9.223.372.036.854.775.808 (-2
^ 63) como un literal entero decimal (literales enteros).
Negación de punto flotante:
El resultado se calcula restando x de cero. La negación decimal es equivalente a usar el operador unario
menos de tipo System.Decimal .
Operador de negación lógica.
En el caso de una operación !x con el formato, se aplica la resolución de sobrecargas de operador unario
(resolución de sobrecarga del operador unario) para seleccionar una implementación de operador específica. El
operando se convierte al tipo de parámetro del operador seleccionado y el tipo del resultado es el tipo de valor
devuelto del operador. Solo existe un operador lógico de negación predefinido:
Este operador calcula la negación lógica del operando: Si el operando es true , el resultado es false . Si el
operando es false , el resultado es true .
Operador de complemento bit a bit
En el caso de una operación ~x con el formato, se aplica la resolución de sobrecargas de operador unario
(resolución de sobrecarga del operador unario) para seleccionar una implementación de operador específica. El
operando se convierte al tipo de parámetro del operador seleccionado y el tipo del resultado es el tipo de valor
devuelto del operador. Los operadores de complemento bit a bit predefinidos son:
El resultado de evaluar ~x , donde x es una expresión de un tipo de enumeración E con un tipo subyacente
U , es exactamente igual que la evaluación (E)(~(U)x) , con la excepción de que la conversión a E siempre se
realiza como si estuviera en un unchecked contexto (operadores comprobados y sin comprobar).
Operadores de incremento y decremento prefijos
pre_increment_expression
: '++' unary_expression
;
pre_decrement_expression
: '--' unary_expression
;
El operando de una operación de incremento o decremento de prefijo debe ser una expresión clasificada como
una variable, un acceso a la propiedad o un acceso a un indexador. El resultado de la operación es un valor del
mismo tipo que el operando.
Si el operando de una operación de incremento o decremento de prefijo es una propiedad o un indexador, la
propiedad o el indexador deben tener un get set descriptor de acceso y. Si no es así, se produce un error en
tiempo de enlace.
La resolución de sobrecargas de operador unario (resolución de sobrecarga de operadores unarios) se aplica
para seleccionar una implementación de operador específica. ++ Existen operadores y predefinidos -- para los
tipos siguientes: sbyte , byte , short , ushort , int , uint ,,, long ulong char , float , double ,
decimal y cualquier tipo de enumeración. Los operadores predefinidos ++ devuelven el valor generado
agregando 1 al operando y los operadores predefinidos -- devuelven el valor generado restando 1 del
operando. En un checked contexto, si el resultado de esta suma o resta está fuera del intervalo del tipo de
resultado y el tipo de resultado es un tipo entero o de enumeración, System.OverflowException se produce una
excepción.
El procesamiento en tiempo de ejecución de una operación de incremento o decremento de prefijo del
formulario ++x o --x consta de los siguientes pasos:
Si xse clasifica como una variable:
x se evalúa para generar la variable.
El operador seleccionado se invoca con el valor de x como argumento.
El valor devuelto por el operador se almacena en la ubicación especificada por la evaluación de x .
El valor devuelto por el operador se convierte en el resultado de la operación.
Si x se clasifica como un acceso de propiedad o indizador:
La expresión de instancia (si x no es static ) y la lista de argumentos (si x es un acceso de
indexador) asociadas a x se evalúan, y los resultados se usan en las get set llamadas a
descriptores de acceso y posteriores.
get Se invoca el descriptor de acceso de x .
El operador seleccionado se invoca con el valor devuelto por el get descriptor de acceso como su
argumento.
El set descriptor de acceso de x se invoca con el valor devuelto por el operador como su value
argumento.
El valor devuelto por el operador se convierte en el resultado de la operación.
Los ++ -- operadores y también admiten la notación de postfijo (operadores de incremento y decremento de
postfijo). Normalmente, el resultado de x++ o x-- es el valor de x antes de la operación, mientras que el
resultado de ++x o --x es el valor de x después de la operación. En cualquier caso, x tiene el mismo valor
después de la operación.
Una operator++ implementación de o operator-- se puede invocar mediante la notación de prefijo o postfijo.
No es posible tener implementaciones de operador independientes para las dos notaciones.
Expresiones de conversión
Un cast_expression se utiliza para convertir explícitamente una expresión a un tipo determinado.
cast_expression
: '(' type ')' unary_expression
;
Cast_expression del formulario (T)E , donde T es un tipo y E es un unary_expression, realiza una conversión
explícita (conversiones explícitas) del valor de E en el tipo T . Si no existe una conversión explícita de E a T ,
se produce un error en tiempo de enlace. De lo contrario, el resultado es el valor generado por la conversión
explícita. El resultado siempre se clasifica como un valor, incluso si E denota una variable.
La gramática de una cast_expression conduce a ciertas ambigüedades sintácticas. Por ejemplo, la expresión
(x)-y se puede interpretar como un cast_expression (una conversión de -y a tipo x ) o como un
additive_expression combinado con un parenthesized_expression (que calcula el valor x - y) .
Para resolver las ambigüedades de cast_expression , existe la siguiente regla: una secuencia de uno o más
tokens(espacio en blanco) entre paréntesis se considera el inicio de un cast_expression solo si se cumple al
menos una de las siguientes condiciones:
La secuencia de tokens es la gramática correcta para un tipo, pero no para una expresión.
La secuencia de tokens es la gramática correcta para un tipo, y el token que sigue inmediatamente al
paréntesis de cierre es el token " ~ ", el token " ! ", el token " ( ", un identificador (secuencias de escape
de caracteres Unicode), un literal (literales) o cualquier palabra clave (Keywords) excepto as y is .
El término "gramática correcta" anterior significa que la secuencia de tokens debe ajustarse a la producción
gramatical concreta. En concreto, no se tiene en cuenta el significado real de los identificadores constituyentes.
Por ejemplo, si x y y son identificadores, x.y es la gramática correcta para un tipo, incluso si x.y no denota
realmente un tipo.
En la regla de desambiguación se sigue que, si x y y son identificadores,, (x)y (x)(y) y (x)(-y) son
cast_expression s, pero (x)-y no es, aunque x identifique un tipo. Sin embargo, si x es una palabra clave que
identifica un tipo predefinido (como int ), las cuatro formas son cast_expression s (porque tal palabra clave
podría no ser una expresión por sí misma).
Expresiones Await
El operador Await se usa para suspender la evaluación de la función asincrónica envolvente hasta que se haya
completado la operación asincrónica representada por el operando.
await_expression
: 'await' unary_expression
;
Solo se permite un await_expression en el cuerpo de una función asincrónica (funciones asincrónicas). Dentro
de la función asincrónica envolvente más cercana, puede que no se produzca una await_expression en estos
lugares:
Dentro de una función anónima anidada (no asincrónica)
Dentro del bloque de un lock_statement
En un contexto no seguro
Tenga en cuenta que no se puede producir un await_expression en la mayoría de los lugares de una
query_expression, porque se transforman sintácticamente para usar expresiones lambda no asincrónicas.
Dentro de una función asincrónica, await no se puede usar como identificador. Por lo tanto, no hay
ambigüedades sintácticas entre las expresiones Await y varias expresiones que intervienen en identificadores.
Fuera de las funciones asincrónicas, await actúa como un identificador normal.
El operando de un await_expression se denomina *Task _. Representa una operación asincrónica que puede o
no completarse en el momento en que se evalúa el _await_expression *. El propósito del operador Await es
suspender la ejecución de la función asincrónica envolvente hasta que se complete la tarea en espera y, a
continuación, obtener el resultado.
Expresiones que admiten Await
Es necesario que la tarea de una expresión Await sea Await . Una expresión t es de esperable si una de las
siguientes contiene:
t es del tipo de tiempo de compilación dynamic
t tiene una instancia accesible o un método de extensión llamado sin GetAwaiter parámetros y sin
parámetros de tipo, y un tipo de valor devuelto para el que se retienen A todos los elementos siguientes:
A implementa la interfaz System.Runtime.CompilerServices.INotifyCompletion (en adelante conocida
como INotifyCompletion por motivos de brevedad)
A tiene una propiedad de instancia accesible y legible IsCompleted de tipo bool
A tiene un método de instancia accesible sin GetResult parámetros y sin parámetros de tipo
El propósito del GetAwaiter método es obtener un *Await _ para la tarea. El tipo A se denomina el tipo _
awaiter* para la expresión Await.
El propósito de la IsCompleted propiedad es determinar si la tarea ya se ha completado. Si es así, no es
necesario suspender la evaluación.
El propósito del INotifyCompletion.OnCompleted método es registrar una "continuación" en la tarea; es decir, un
delegado (de tipo System.Action ) que se invocará una vez que se complete la tarea.
El propósito del GetResult método es obtener el resultado de la tarea una vez que ha finalizado. Este resultado
puede ser una finalización correcta, posiblemente con un valor de resultado, o puede ser una excepción
producida por el GetResult método.
Clasificación de expresiones Await
La expresión await t se clasifica de la misma forma que la expresión (t).GetAwaiter().GetResult() . Por lo
tanto, si el tipo de valor devuelto de GetResult es void , el await_expression se clasifica como Nothing. Si tiene
un tipo de valor devuelto distinto de void T , el await_expression se clasifica como un valor de tipo T .
Evaluación en tiempo de ejecución de expresiones Await
En tiempo de ejecución, la expresión await t se evalúa como sigue:
Un Await ase obtiene evaluando la expresión (t).GetAwaiter() .
bool b Se obtiene mediante la evaluación de la expresión (a).IsCompleted .
Si b es false , la evaluación depende de si a implementa la interfaz
System.Runtime.CompilerServices.ICriticalNotifyCompletion (en adelante conocida como
ICriticalNotifyCompletion por motivos de brevedad). Esta comprobación se realiza en el momento del
enlace; es decir, en tiempo de ejecución si a tiene el tipo de tiempo de compilación dynamic y en tiempo de
compilación, en caso contrario. Se r debe indicar el delegado de reanudación (funciones asincrónicas):
Si no a implementa ICriticalNotifyCompletion , se evalúa la expresión
(a as (INotifyCompletion)).OnCompleted(r) .
Si a implementa ICriticalNotifyCompletion , se evalúa la expresión
(a as (ICriticalNotifyCompletion)).UnsafeOnCompleted(r) .
A continuación, la evaluación se suspende y el control se devuelve al autor de la llamada actual de la
función asincrónica.
Inmediatamente después de (si b era true ), o después de la invocación posterior del delegado de
reanudación (si b era false ), (a).GetResult() se evalúa la expresión. Si devuelve un valor, ese valor es el
resultado de la await_expression. De lo contrario, el resultado es Nothing.
La implementación de un espera de los métodos de interfaz INotifyCompletion.OnCompleted y
ICriticalNotifyCompletion.UnsafeOnCompleted debe hacer que r se invoque el delegado como máximo una vez.
De lo contrario, el comportamiento de la función asincrónica envolvente es indefinido.
Operadores aritméticos
Los * / operadores,, % , + y - se denominan operadores aritméticos.
multiplicative_expression
: unary_expression
| multiplicative_expression '*' unary_expression
| multiplicative_expression '/' unary_expression
| multiplicative_expression '%' unary_expression
;
additive_expression
: multiplicative_expression
| additive_expression '+' multiplicative_expression
| additive_expression '-' multiplicative_expression
;
Si un operando de un operador aritmético tiene el tipo en tiempo de compilación dynamic , la expresión está
enlazada dinámicamente (enlace dinámico). En este caso, el tipo en tiempo de compilación de la expresión es
dynamic y la resolución que se describe a continuación se realiza en tiempo de ejecución mediante el tipo en
tiempo de ejecución de los operandos que tienen el tipo en tiempo de compilación dynamic .
Operador de multiplicación
En el caso de una operación x * y con el formato, se aplica la resolución de sobrecargas del operador binario
(resolución de sobrecarga del operador binario) para seleccionar una implementación de operador específica.
Los operandos se convierten en los tipos de parámetro del operador seleccionado y el tipo del resultado es el
tipo de valor devuelto del operador.
A continuación se enumeran los operadores de multiplicación predefinidos. Todos los operadores calculan el
producto de x y y .
Multiplicación de enteros:
El producto se calcula de acuerdo con las reglas de aritmética de IEEE 754. En la tabla siguiente se
enumeran los resultados de todas las posibles combinaciones de valores finitos distintos de cero, ceros,
infinitos y NaN. En la tabla, x e y son valores finitos positivos. z es el resultado de x * y . Si el
resultado es demasiado grande para el tipo de destino, z es infinito. Si el resultado es demasiado
pequeño para el tipo de destino, z es cero.
Multiplicación decimal:
El cociente se calcula de acuerdo con las reglas de aritmética de IEEE 754. En la tabla siguiente se
enumeran los resultados de todas las posibles combinaciones de valores finitos distintos de cero, ceros,
infinitos y NaN. En la tabla, x e y son valores finitos positivos. z es el resultado de x / y . Si el
resultado es demasiado grande para el tipo de destino, z es infinito. Si el resultado es demasiado
pequeño para el tipo de destino, z es cero.
División decimal:
En la tabla siguiente se enumeran los resultados de todas las posibles combinaciones de valores finitos
distintos de cero, ceros, infinitos y NaN. En la tabla, x e y son valores finitos positivos. z es el
resultado de x % y y se calcula como x - n * y , donde n es el entero más grande posible que es
menor o igual que x / y . Este método de calcular el resto es análogo al que se usa para los operandos
de entero, pero difiere de la definición de IEEE 754 (en que n es el entero más próximo a x / y ).
Resto decimal:
La suma se calcula de acuerdo con las reglas de aritmética de IEEE 754. En la tabla siguiente se enumeran
los resultados de todas las posibles combinaciones de valores finitos distintos de cero, ceros, infinitos y
NaN. En la tabla, x e y son valores finitos distintos de cero y z es el resultado de x + y . Si x e y
tienen la misma magnitud pero signos opuestos, z es cero positivo. Si x + y es demasiado grande
para representarlo en el tipo de destino, z es un infinito con el mismo signo que x + y .
y +0 -0 +inf -inf NaN
Suma decimal:
class Test
{
static void Main() {
string s = null;
Console.WriteLine("s = >" + s + "<"); // displays s = ><
int i = 1;
Console.WriteLine("i = " + i); // displays i = 1
float f = 1.2300E+15F;
Console.WriteLine("f = " + f); // displays f = 1.23E+15
decimal d = 2.900m;
Console.WriteLine("d = " + d); // displays d = 2.900
}
}
El resultado del operador de concatenación de cadenas es una cadena que consta de los caracteres del
operando izquierdo seguidos de los caracteres del operando derecho. El operador de concatenación de
cadenas nunca devuelve un null valor. System.OutOfMemoryException Se puede producir una excepción si
no hay suficiente memoria disponible para asignar la cadena resultante.
Combinación de delegado. Cada tipo de delegado proporciona implícitamente el siguiente operador
predefinido, donde D es el tipo de delegado:
El operador binario + realiza la combinación de delegados cuando ambos operandos son de algún tipo
de delegado D . (Si los operandos tienen distintos tipos de delegado, se produce un error en tiempo de
enlace). Si el primer operando es null , el resultado de la operación es el valor del segundo operando
(aunque también sea null ). De lo contrario, si el segundo operando es null , el resultado de la
operación es el valor del primer operando. De lo contrario, el resultado de la operación es una nueva
instancia de delegado que, cuando se invoca, invoca el primer operando y, a continuación, invoca el
segundo operando. Para obtener ejemplos de la combinación de delegados, vea operador de resta e
invocación de delegado. Puesto que System.Delegate no es un tipo de delegado, operator + no está
definido para él.
Operador de resta
En el caso de una operación x - y con el formato, se aplica la resolución de sobrecargas del operador binario
(resolución de sobrecarga del operador binario) para seleccionar una implementación de operador específica.
Los operandos se convierten en los tipos de parámetro del operador seleccionado y el tipo del resultado es el
tipo de valor devuelto del operador.
A continuación se enumeran los operadores de resta predefinidos. Los operadores se restann y de x .
Resta de enteros:
En un checked contexto, si la diferencia está fuera del intervalo del tipo de resultado,
System.OverflowException se produce una excepción. En un unchecked contexto, no se informan los
desbordamientos y se descartan los bits significativos de orden superior fuera del intervalo del tipo de
resultado.
Resta de punto flotante:
La diferencia se calcula de acuerdo con las reglas de aritmética de IEEE 754. En la tabla siguiente se
enumeran los resultados de todas las posibles combinaciones de valores finitos distintos de cero, ceros,
infinitos y Nan. En la tabla, x e y son valores finitos distintos de cero y z es el resultado de x - y . Si
x e y son iguales, z es cero positivo. Si x - y es demasiado grande para representarlo en el tipo de
destino, z es un infinito con el mismo signo que x - y .
Resta decimal:
Este operador se evalúa exactamente como (U)((U)x - (U)y) . En otras palabras, el operador calcula la
diferencia entre los valores ordinales de x y y , y el tipo del resultado es el tipo subyacente de la
enumeración.
Este operador se evalúa exactamente como (E)((U)x - y) . En otras palabras, el operador resta un valor
del tipo subyacente de la enumeración, lo que produce un valor de la enumeración.
Eliminación de delegados. Cada tipo de delegado proporciona implícitamente el siguiente operador
predefinido, donde D es el tipo de delegado:
El operador binario - realiza la eliminación del delegado cuando ambos operandos son de algún tipo de
delegado D . Si los operandos tienen distintos tipos de delegado, se produce un error en tiempo de
enlace. Si el primer operando es null , el resultado de la operación es null . De lo contrario, si el
segundo operando es null , el resultado de la operación es el valor del primer operando. De lo
contrario, ambos operandos representan listas de invocación (declaraciones de delegado) que tienen una
o más entradas y el resultado es una nueva lista de invocación que se compone de la lista del primer
operando con las entradas del segundo operando que se han quitado, siempre que la lista del segundo
operando sea una sublista contigua adecuada de la primera. (Para determinar la igualdad de la lista, las
entradas correspondientes se comparan como para el operador de igualdad de delegado (operadores de
igualdad de delegado)). De lo contrario, el resultado es el valor del operando izquierdo. En el proceso no
se cambia ninguna de las listas de operandos. Si la lista del segundo operando coincide con varias
sublistas de entradas contiguas de la lista del primer operando, se quita la sublista coincidente situada
más a la derecha de las entradas contiguas. Si la eliminación da como resultado una lista vacía, el
resultado es null . Por ejemplo:
class C
{
public static void M1(int i) { /* ... */ }
public static void M2(int i) { /* ... */ }
}
class Test
{
static void Main() {
D cd1 = new D(C.M1);
D cd2 = new D(C.M2);
D cd3 = cd1 + cd2 + cd2 + cd1; // M1 + M2 + M2 + M1
cd3 -= cd1; // => M1 + M2 + M2
Operadores de desplazamiento
Los << >> operadores y se utilizan para realizar operaciones de desplazamiento de bits.
shift_expression
: additive_expression
| shift_expression '<<' additive_expression
| shift_expression right_shift additive_expression
;
Si un operando de una shift_expression tiene el tipo en tiempo de compilación dynamic , la expresión está
enlazada dinámicamente (enlace dinámico). En este caso, el tipo en tiempo de compilación de la expresión es
dynamic y la resolución que se describe a continuación se realiza en tiempo de ejecución mediante el tipo en
tiempo de ejecución de los operandos que tienen el tipo en tiempo de compilación dynamic .
Para una operación con el formato x << count o x >> count , se aplica la resolución de sobrecargas del
operador binario (resolución de sobrecarga del operador binario) para seleccionar una implementación de
operador específica. Los operandos se convierten en los tipos de parámetro del operador seleccionado y el tipo
del resultado es el tipo de valor devuelto del operador.
Al declarar un operador de desplazamiento sobrecargado, el tipo del primer operando siempre debe ser la clase
o el struct que contiene la declaración del operador y el tipo del segundo operando siempre debe ser int .
A continuación se enumeran los operadores de desplazamiento predefinidos.
Desplazar a la izquierda:
Las operaciones de desplazamiento nunca causan desbordamientos y producen los mismos resultados en
checked y unchecked contextos.
Cuando el operando izquierdo del >> operador es de un tipo entero con signo, el operador realiza un
desplazamiento aritmético a la derecha, donde el valor del bit más significativo (el bit de signo) del operando se
propaga a las posiciones de bits vacías de orden superior. Cuando el operando izquierdo del >> operador es de
un tipo entero sin signo, el operador realiza un desplazamiento lógico derecho en el que las posiciones de bits
vacías de orden superior siempre se establecen en cero. Para realizar la operación opuesta a la que se infiere del
tipo de operando, se pueden usar conversiones explícitas. Por ejemplo, si x es una variable de tipo int , la
operación unchecked((int)((uint)x >> y)) realiza un desplazamiento lógico a la derecha de x .
relational_expression
: shift_expression
| relational_expression '<' shift_expression
| relational_expression '>' shift_expression
| relational_expression '<=' shift_expression
| relational_expression '>=' shift_expression
| relational_expression 'is' type
| relational_expression 'as' type
;
equality_expression
: relational_expression
| equality_expression '==' relational_expression
| equality_expression '!=' relational_expression
;
Cada uno de estos operadores compara los valores numéricos de los dos operandos de tipo entero y devuelve
un bool valor que indica si la relación concreta es true o false .
Operadores de comparación de punto flotante
Los operadores de comparación de punto flotante predefinidos son:
bool operator ==(float x, float y);
bool operator ==(double x, double y);
Los operadores comparan los operandos según las reglas del estándar IEEE 754:
Si alguno de los operandos es NaN, el resultado es false para todos los operadores excepto != , para
los que el resultado es true . Para dos operandos cualesquiera, x != y siempre genera el mismo
resultado que !(x == y) . Sin embargo, cuando uno o ambos operandos son Nan, los < operadores,,
> <= y >= no producen los mismos resultados que la negación lógica del operador opuesto. Por
ejemplo, si uno de los valores de x y y es Nan, x < y es false , pero !(x >= y) es true .
Cuando ninguno de los operandos es NaN, los operadores comparan los valores de los dos operandos
de punto flotante con respecto a la ordenación.
-inf < -max < ... < -min < -0.0 == +0.0 < +min < ... < +max < +inf
donde min y max son los valores finitos positivos más pequeños y mayores que se pueden representar
en el formato de punto flotante dado. Los efectos importantes de este orden son:
Los ceros negativos y positivos se consideran iguales.
Un infinito negativo se considera menor que todos los demás valores, pero es igual a otro infinito
negativo.
Un infinito positivo se considera mayor que el resto de los valores, pero es igual a otro infinito
positivo.
Operadores de comparación decimal
Los operadores de comparación decimal predefinidos son:
Cada uno de estos operadores compara los valores numéricos de los dos operandos decimales y devuelve un
bool valor que indica si la relación concreta es true o false . Cada comparación decimal es equivalente a
usar el operador relacional o de igualdad correspondiente de tipo System.Decimal .
Operadores de igualdad booleanos
Los operadores de igualdad booleano predefinidos son:
bool operator ==(bool x, bool y);
bool operator !=(bool x, bool y);
Los operadores devuelven el resultado de comparar las dos referencias de igualdad o de no igualdad.
Puesto que los operadores de igualdad de tipos de referencia predefinidos aceptan operandos de tipo object ,
se aplican a todos los tipos que no declaran operator == operator != los miembros y aplicables. Por el
contrario, todos los operadores de igualdad definidos por el usuario aplicables ocultan de forma eficaz los
operadores de igualdad de tipos de referencia predefinidos.
Los operadores de igualdad de tipos de referencia predefinidos requieren uno de los siguientes:
Ambos operandos son un valor de un tipo conocido como reference_type o literal null . Además, existe una
conversión de referencia explícita (conversiones de referencia explícitas) desde el tipo de uno de los
operandos al tipo del otro operando.
Un operando es un valor de T tipo T , donde es un type_parameter y el otro operando es el literal null .
Además, no T tiene la restricción de tipo de valor.
A menos que se cumpla una de estas condiciones, se produce un error en tiempo de enlace. Las implicaciones
destacadas de estas reglas son:
Es un error en tiempo de enlace usar los operadores de igualdad de tipos de referencia predefinidos para
comparar dos referencias que se sabe que son diferentes en tiempo de enlace. Por ejemplo, si los tipos en
tiempo de enlace de los operandos son dos tipos de clase A y B , y si no se A B derivan de la otra, sería
imposible que los dos operandos hagan referencia al mismo objeto. Por lo tanto, la operación se considera
un error en tiempo de enlace.
Los operadores de igualdad de tipos de referencia predefinidos no permiten comparar los operandos de tipo
de valor. Por lo tanto, a menos que un tipo de estructura declare sus propios operadores de igualdad, no es
posible comparar valores de ese tipo de estructura.
Los operadores de igualdad de tipos de referencia predefinidos nunca provocan que se produzcan
operaciones de conversión boxing para sus operandos. No tendría sentido realizar estas operaciones de
conversión boxing, ya que las referencias a las instancias de conversión boxing recién asignadas serían
necesariamente distintas de las demás referencias.
Si un operando de un tipo de parámetro de tipo T se compara con null y el tipo en tiempo de ejecución
de T es un tipo de valor, el resultado de la comparación es false .
class C<T>
{
void F(T x) {
if (x == null) throw new ArgumentNullException();
...
}
}
La x == null construcción se permite aunque T pueda representar un tipo de valor y el resultado se define
simplemente como false cuando T es un tipo de valor.
En el caso de una operación de la forma x == y o x != y , si es aplicable operator == o operator != existe,
las reglas de resolución de sobrecarga del operador (resolución de sobrecarga del operador binario)
seleccionarán ese operador en lugar del operador de igualdad de tipos de referencia predefinido. Sin embargo,
siempre es posible seleccionar el operador de igualdad de tipos de referencia predefinido convirtiendo
explícitamente uno o ambos operandos al tipo object . En el ejemplo
using System;
class Test
{
static void Main() {
string s = "Test";
string t = string.Copy(s);
Console.WriteLine(s == t);
Console.WriteLine((object)s == t);
Console.WriteLine(s == (object)t);
Console.WriteLine((object)s == (object)t);
}
}
genera el resultado
True
False
False
False
Las s t variables y hacen referencia a dos string instancias distintas que contienen los mismos caracteres.
La primera comparación genera resultados True porque el operador de igualdad de cadena predefinido
(operadores de igualdad de cadena) está seleccionado cuando ambos operandos son de tipo string . El resto
de las comparaciones False se generan porque el operador de igualdad de tipos de referencia predefinido se
selecciona cuando uno o ambos operandos son del tipo object .
Tenga en cuenta que la técnica anterior no es significativa para los tipos de valor. En el ejemplo
class Test
{
static void Main() {
int i = 123;
int j = 123;
System.Console.WriteLine((object)i == (object)j);
}
}
False se produce porque las conversiones crean referencias a dos instancias independientes de valores de
conversión boxing int .
Operadores de igualdad de cadenas
Los operadores de igualdad de cadena predefinidos son:
Dos string valores se consideran iguales cuando se cumple una de las siguientes condiciones:
Ambos valores son null .
Ambos valores son referencias no nulas a instancias de cadena que tienen longitudes idénticas y caracteres
idénticos en cada posición de carácter.
Los operadores de igualdad de cadena comparan valores de cadena en lugar de referencias de cadena. Cuando
dos instancias de cadena independientes contienen exactamente la misma secuencia de caracteres, los valores
de las cadenas son iguales, pero las referencias son diferentes. Como se describe en operadores de igualdad de
tipos de referencia, los operadores de igualdad de tipos de referencia se pueden usar para comparar referencias
de cadena en lugar de valores de cadena.
Operadores de igualdad de delegado
Cada tipo de delegado proporciona implícitamente los siguientes operadores de comparación predefinidos:
x == null
null == x
x != null
null != x
donde x es una expresión de un tipo que acepta valores NULL, si la resolución de sobrecarga del operador
(resolución de sobrecarga del operador binario) no encuentra un operador aplicable, el resultado se calcula en
su lugar a partir de la HasValue propiedad de x . En concreto, las dos primeras formas se traducen en
!x.HasValue y se traducen las dos últimas x.HasValue .
El operador is
El is operador se usa para comprobar dinámicamente si el tipo en tiempo de ejecución de un objeto es
compatible con un tipo determinado. El resultado de la operación E is T , donde E es una expresión y T es
un tipo, es un valor booleano que indica si se E puede convertir correctamente al tipo T mediante una
conversión de referencia, una conversión boxing o una conversión unboxing. La operación se evalúa como
sigue, después de que se hayan sustituido los argumentos de tipo para todos los parámetros de tipo:
Si E es una función anónima, se produce un error en tiempo de compilación
Si E es un grupo de métodos o el null literal, si el tipo de E es un tipo de referencia o un tipo que acepta
valores NULL y el valor de E es null, el resultado es false.
En caso contrario, deje que D represente el tipo dinámico de de la E siguiente manera:
Si el tipo de E es un tipo de referencia, D es el tipo en tiempo de ejecución de la referencia de
instancia de E .
Si el tipo de E es un tipo que acepta valores NULL, D es el tipo subyacente de ese tipo que acepta
valores NULL.
Si el tipo de E es un tipo de valor que no acepta valores NULL, D es el tipo de E .
El resultado de la operación depende de y de la manera D T siguiente:
Si T es un tipo de referencia, el resultado es true si D y T son del mismo tipo, si D es un tipo de
referencia y una conversión de referencia implícita de D a T EXISTS, o si D es un tipo de valor y una
conversión boxing de D a T EXISTS.
Si T es un tipo que acepta valores NULL, el resultado es true si D es el tipo subyacente de T .
Si T es un tipo de valor que no acepta valores NULL, el resultado es true si D y T son del mismo
tipo.
De lo contrario, el resultado es false.
Tenga en cuenta que las conversiones definidas por el usuario no se tienen en cuenta por el is operador.
El operador as
El as operador se usa para convertir explícitamente un valor en un tipo de referencia determinado o un tipo
que acepta valores NULL. A diferencia de una expresión de conversión (expresiones de conversión), el as
operador nunca produce una excepción. En su lugar, si la conversión indicada no es posible, el valor resultante
es null .
En una operación del formulario E as T , E debe ser una expresión y T debe ser un tipo de referencia, un
parámetro de tipo conocido como un tipo de referencia o un tipo que acepta valores NULL. Además, al menos
uno de los siguientes debe ser true o, de lo contrario, se producirá un error en tiempo de compilación:
Una identidad (conversión de identidad), implícita que acepta valores NULL (conversiones implícitas que
aceptan valores NULL), referencia implícita (conversiones dereferencias implícitas), conversión boxing
(conversiones boxing), conversión explícita de valores NULL (conversionesexplícitas que aceptan valores
NULL), referencia explícita (conversiones dereferencia explícita) o conversión unboxing
(conversionesunboxing) de E a T .
El tipo de E o T es un tipo abierto.
E es el null literal.
E is T ? (T)(E) : (T)null
salvo que E solo se evalúa una vez. Se puede esperar que el compilador optimice E as T para realizar como
máximo una comprobación de tipos dinámicos en lugar de las dos comprobaciones de tipos dinámicos
implícitas por la expansión anterior.
Si el tipo en tiempo de compilación de E es dynamic , a diferencia del operador de conversión, el as operador
no está enlazado dinámicamente (enlace dinámico). Por lo tanto, la expansión en este caso es:
E is T ? (T)(object)(E) : (T)null
Tenga en cuenta que algunas conversiones, como las conversiones definidas por el usuario, no son posibles con
el as operador y deben realizarse en su lugar mediante expresiones de conversión.
En el ejemplo
class X
{
public U H<U>(object o) {
return o as U; // Error, U is unconstrained
}
}
T se sabe que el parámetro de tipo de G es un tipo de referencia, porque tiene la restricción de clase. U Sin
embargo, el parámetro de tipo de H no es; por lo tanto, no se permite el uso del as operador en H .
Operadores lógicos
Los & ^ operadores, y | se denominan operadores lógicos.
and_expression
: equality_expression
| and_expression '&' equality_expression
;
exclusive_or_expression
: and_expression
| exclusive_or_expression '^' and_expression
;
inclusive_or_expression
: exclusive_or_expression
| inclusive_or_expression '|' exclusive_or_expression
;
Si un operando de un operador lógico tiene el tipo en tiempo de compilación dynamic , la expresión está
enlazada dinámicamente (enlace dinámico). En este caso, el tipo en tiempo de compilación de la expresión es
dynamic y la resolución que se describe a continuación se realiza en tiempo de ejecución mediante el tipo en
tiempo de ejecución de los operandos que tienen el tipo en tiempo de compilación dynamic .
En el caso de una operación con el formato x op y , donde op es uno de los operadores lógicos, se aplica la
resolución de sobrecarga (resolución de sobrecarga del operador binario) para seleccionar una implementación
de operador específica. Los operandos se convierten en los tipos de parámetro del operador seleccionado y el
tipo del resultado es el tipo de valor devuelto del operador.
En las secciones siguientes se describen los operadores lógicos predefinidos.
Operadores lógicos enteros
Los operadores lógicos de enteros predefinidos son:
El operador calcula el lógico bit AND a bit de los dos operandos, el | operador calcula el operador lógico bit
&
OR a bit de los dos operandos y el ^ operador calcula la lógica exclusiva bit OR a bit de los dos operandos. No
es posible realizar desbordamientos en estas operaciones.
Operadores lógicos de enumeración
Cada tipo de enumeración E proporciona implícitamente los siguientes operadores lógicos predefinidos:
El resultado de x & y es true si tanto x como y son true . De lo contrario, el resultado es false .
El resultado de x | y es true si x o y es true . De lo contrario, el resultado es false .
El resultado de x ^ y es true si x es true y y es false , o x es false y y es true . De lo contrario, el
resultado es false . Cuando los operandos son de tipo bool , el ^ operador calcula el mismo resultado que el
!= operador.
En la tabla siguiente se enumeran los resultados generados por estos operadores para todas las combinaciones
de los valores true , false y null .
X Y X & Y X | Y
conditional_and_expression
: inclusive_or_expression
| conditional_and_expression '&&' inclusive_or_expression
;
conditional_or_expression
: conditional_and_expression
| conditional_or_expression '||' conditional_and_expression
;
Si un operando de un operador lógico condicional tiene el tipo en tiempo de compilación dynamic , la expresión
está enlazada dinámicamente (enlace dinámico). En este caso, el tipo en tiempo de compilación de la expresión
es dynamic y la resolución que se describe a continuación se realiza en tiempo de ejecución mediante el tipo en
tiempo de ejecución de los operandos que tienen el tipo en tiempo de compilación dynamic .
Una operación con el formato x && y o x || y se procesa aplicando la resolución de sobrecarga (resolución
de sobrecarga del operador binario) como si la operación se hubiera escrito x & y o x | y . A continuación,
Si la resolución de sobrecarga no encuentra un único operador mejor, o si la resolución de sobrecarga
selecciona uno de los operadores lógicos de enteros predefinidos, se produce un error en tiempo de enlace.
De lo contrario, si el operador seleccionado es uno de los operadores lógicos booleanos predefinidos
(operadores lógicos booleanos) o los operadores lógicos booleanos que aceptan valores NULL (operadores
lógicosbooleanos que aceptan valores NULL), la operación se procesa como se describe en operadores
lógicos condicionales booleanos.
De lo contrario, el operador seleccionado es un operador definido por el usuario y la operación se procesa
como se describe en operadores lógicos condicionales definidos por el usuario.
No es posible sobrecargar directamente los operadores lógicos condicionales. Sin embargo, dado que los
operadores lógicos condicionales se evalúan en términos de los operadores lógicos normales, las sobrecargas
de los operadores lógicos normales son, con ciertas restricciones, que también se consideran sobrecargas de los
operadores lógicos condicionales. Esto se describe con más detalle en operadores lógicos condicionales
definidos por el usuario.
Operadores lógicos condicionales booleanos
Cuando los operandos de && o || son de tipo bool , o cuando los operandos son de tipos que no definen un
aplicable operator & o operator | , pero definen conversiones implícitas en bool , la operación se procesa de
la siguiente manera:
La operación x && y se evalúa como x ? y : false . En otras palabras, x se evalúa primero y se convierte
al tipo bool . Después, si x es true , y se evalúa y se convierte al tipo bool , y se convierte en el
resultado de la operación. De lo contrario, el resultado de la operación es false .
La operación x || y se evalúa como x ? true : y . En otras palabras, x se evalúa primero y se convierte
al tipo bool . Después, si x es true , el resultado de la operación es true . De lo contrario, y se evalúa y
se convierte al tipo bool , y se convierte en el resultado de la operación.
Operadores lógicos condicionales definidos por el usuario
Cuando los operandos de && o || son de tipos que declaran un o definido por operator & el usuario
aplicable, deben cumplirse operator | las dos condiciones siguientes, donde T es el tipo en el que se declara
el operador seleccionado:
El tipo de valor devuelto y el tipo de cada parámetro del operador seleccionado deben ser T . En otras
palabras, el operador debe calcular la lógica AND or lógica OR de dos operandos de tipo T y debe devolver
un resultado de tipo T .
T debe contener declaraciones de operator true y operator false .
Se produce un error en tiempo de enlace si no se cumple alguno de estos requisitos. De lo contrario, la && ||
operación OR se evalúa combinando el definido por el usuario operator true o operator false con el
operador definido por el usuario seleccionado:
La operación x && y se evalúa como T.false(x) ? x : T.&(x, y) , donde T.false(x) es una invocación del
operator false declarado en T , y T.&(x, y) es una invocación del seleccionado operator & . En otras
palabras, x se evalúa primero y operator false se invoca en el resultado para determinar si x es false
definitivamente. Después, si x es definitivamente false, el resultado de la operación es el valor previamente
calculado para x . De lo contrario, y se evalúa y el seleccionado operator & se invoca en el valor calculado
previamente para x y el valor calculado para y para generar el resultado de la operación.
La operación x || y se evalúa como T.true(x) ? x : T.|(x, y) , donde T.true(x) es una invocación del
operator true declarado en T , y T.|(x,y) es una invocación del seleccionado operator| . En otras
palabras, x se evalúa primero y operator true se invoca en el resultado para determinar si x es true
definitivamente. A continuación, si x es true definitivamente, el resultado de la operación es el valor
previamente calculado para x . De lo contrario, y se evalúa y el seleccionado operator | se invoca en el
valor calculado previamente para x y el valor calculado para y para generar el resultado de la operación.
En cualquiera de estas operaciones, la expresión proporcionada por x solo se evalúa una vez, y la expresión
proporcionada por y no se evalúa ni se evalúa exactamente una vez.
Para obtener un ejemplo de un tipo que implementa operator true y operator false , vea Database Boolean
Type.
null_coalescing_expression
: conditional_or_expression
| conditional_or_expression '??' null_coalescing_expression
;
Una expresión de fusión nula del formulario a ?? b requiere a que sea de un tipo que acepte valores NULL o
un tipo de referencia. Si a no es null, el resultado de a ?? b es a ; de lo contrario, el resultado es b . La
operación b solo se evalúa si a es NULL.
El operador de uso combinado de NULL es asociativo a la derecha, lo que significa que las operaciones se
agrupan de derecha a izquierda. Por ejemplo, una expresión con el formato a ?? b ?? c se evalúa como
a ?? (b ?? c) . En términos generales, una expresión con el formato E1 ?? E2 ?? ... ?? En devuelve el
primero de los operandos que no son NULL, o null si todos los operandos son NULL.
El tipo de la expresión a ?? b depende de las conversiones implícitas que están disponibles en los operandos.
En orden de preferencia, el tipo de a ?? b es A0 , A o B , donde A es el tipo de a (siempre que a tenga
un tipo), B es el tipo de b (siempre que b tenga un tipo) y A0 es el tipo subyacente de A si A es un tipo
que acepta valores NULL, o de A lo contrario. En concreto, a ?? b se procesa de la siguiente manera:
Si A existe y no es un tipo que acepta valores NULL o un tipo de referencia, se produce un error en tiempo
de compilación.
Si b es una expresión dinámica, el tipo de resultado es dynamic . En tiempo de ejecución, a se evalúa
primero. Si a no es null, a se convierte en dinámico y se convierte en el resultado. De lo contrario, b se
evalúa y se convierte en el resultado.
De lo contrario, si A existe y es un tipo que acepta valores NULL y existe una conversión implícita de b a
A0 , el tipo de resultado es A0 . En tiempo de ejecución, a se evalúa primero. Si a no es null, a se
desencapsulará en A0 el tipo y se convertirá en el resultado. De lo contrario, b se evalúa y se convierte al
tipo A0 , y se convierte en el resultado.
De lo contrario, si A existe y existe una conversión implícita de b a A , el tipo de resultado es A . En
tiempo de ejecución, a se evalúa primero. Si a no es null, a se convierte en el resultado. De lo contrario,
b se evalúa y se convierte al tipo A , y se convierte en el resultado.
De lo contrario, si b tiene un tipo B y existe una conversión implícita de a a B , el tipo de resultado es B
. En tiempo de ejecución, a se evalúa primero. Si a no es null, a se desencapsulará en A0 el tipo (si A
existe y acepta valores NULL) y se convertirá al tipo, lo que se B convierte en el resultado. De lo contrario,
b se evalúa y se convierte en el resultado.
De lo contrario, a y b son incompatibles y se produce un error en tiempo de compilación.
Operador condicional
El ?: operador se denomina operador condicional. En ocasiones también se denomina operador ternario.
conditional_expression
: null_coalescing_expression
| null_coalescing_expression '?' expression ':' expression
;
Una expresión condicional del formulario b ? x : y evalúa primero la condición b . Después, si b es true ,
x se evalúa y se convierte en el resultado de la operación. De lo contrario, y se evalúa y se convierte en el
resultado de la operación. Una expresión condicional nunca evalúa x y y .
El operador condicional es asociativo a la derecha, lo que significa que las operaciones se agrupan de derecha a
izquierda. Por ejemplo, una expresión con el formato a ? b : c ? d : e se evalúa como a ? b : (c ? d : e) .
El primer operando del ?: operador debe ser una expresión que se pueda convertir implícitamente a bool , o
una expresión de un tipo que implemente operator true . Si no se cumple ninguno de estos requisitos, se
produce un error en tiempo de compilación.
Los operandos segundo y tercero, x y y , del ?: operador controlan el tipo de la expresión condicional.
Si x tiene el tipo X y y tiene el tipo Y , entonces
Si existe una conversión implícita (conversiones implícitas) de X a Y , pero no de Y a X , entonces
Y es el tipo de la expresión condicional.
Si existe una conversión implícita (conversiones implícitas) de Y a X , pero no de X a Y , entonces
X es el tipo de la expresión condicional.
De lo contrario, no se puede determinar ningún tipo de expresión y se produce un error en tiempo de
compilación.
Si solo uno de x y y tiene un tipo, y x y y , de se pueden convertir implícitamente a ese tipo, es el tipo
de la expresión condicional.
De lo contrario, no se puede determinar ningún tipo de expresión y se produce un error en tiempo de
compilación.
El procesamiento en tiempo de ejecución de una expresión condicional con el formato b ? x : y consta de los
siguientes pasos:
En primer lugar, b se evalúa y bool se determina el valor de b :
Si una conversión implícita del tipo de b bool existe, esta conversión implícita se realiza para
generar un bool valor.
De lo contrario, el operator true definido por el tipo de b se invoca para generar un bool valor.
Si el bool valor generado por el paso anterior es true , x se evalúa y se convierte al tipo de la expresión
condicional y se convierte en el resultado de la expresión condicional.
De lo contrario, y se evalúa y se convierte al tipo de la expresión condicional y se convierte en el resultado
de la expresión condicional.
anonymous_method_expression
: 'delegate' explicit_anonymous_function_signature? block
;
anonymous_function_signature
: explicit_anonymous_function_signature
| implicit_anonymous_function_signature
;
explicit_anonymous_function_signature
: '(' explicit_anonymous_function_parameter_list? ')'
;
explicit_anonymous_function_parameter_list
: explicit_anonymous_function_parameter (',' explicit_anonymous_function_parameter)*
;
explicit_anonymous_function_parameter
: anonymous_function_parameter_modifier? type identifier
;
anonymous_function_parameter_modifier
: 'ref'
| 'out'
;
implicit_anonymous_function_signature
: '(' implicit_anonymous_function_parameter_list? ')'
| implicit_anonymous_function_parameter
;
implicit_anonymous_function_parameter_list
: implicit_anonymous_function_parameter (',' implicit_anonymous_function_parameter)*
;
implicit_anonymous_function_parameter
: identifier
;
anonymous_function_body
: expression
| block
;
La ItemList<T> clase tiene dos Sum métodos. Cada toma un selector argumento, que extrae el valor que se
va a sumar de un elemento de lista. El valor extraído puede ser int o, double y la suma resultante también es
int o double .
Los Sum métodos se pueden utilizar, por ejemplo, para calcular las sumas de una lista de líneas de detalle en un
pedido.
class Detail
{
public int UnitCount;
public double UnitPrice;
...
}
void ComputeSums() {
ItemList<Detail> orderDetails = GetOrderDetails(...);
int totalUnits = orderDetails.Sum(d => d.UnitCount);
double orderTotal = orderDetails.Sum(d => d.UnitPrice * d.UnitCount);
...
}
En la primera invocación de orderDetails.Sum , ambos Sum métodos son aplicables porque la función anónima
d => d. UnitCount es compatible con Func<Detail,int> y Func<Detail,double> . Sin embargo, la resolución de
sobrecarga selecciona el primer Sum método porque la conversión a Func<Detail,int> es mejor que la
conversión a Func<Detail,double> .
En la segunda invocación de orderDetails.Sum, solo el segundo Sum método es aplicable porque la función
anónima d => d.UnitPrice * d.UnitCount genera un valor de tipo double . Por lo tanto, la resolución de
sobrecarga elige el segundo Sum método para esa invocación.
using System;
class Test
{
static D F() {
int x = 0;
D result = () => ++x;
return result;
}
la x función anónima captura la variable local y la duración de x se extiende al menos hasta que el delegado
devuelto F por se pueda seleccionar para la recolección de elementos no utilizados (que no se produce hasta el
final del programa). Dado que cada invocación de la función anónima funciona en la misma instancia de x , el
resultado del ejemplo es:
1
2
3
Cuando una función anónima captura una variable local o un parámetro de valor, la variable local o el
parámetro ya no se considera una variable fija (variables fijas y móviles), sino que se considera una variable
móvil. Por lo tanto, cualquier unsafe código que toma la dirección de una variable externa capturada debe usar
primero la fixed instrucción para corregir la variable.
Tenga en cuenta que, a diferencia de una variable no capturada, una variable local capturada se puede exponer
simultáneamente a varios subprocesos de ejecución.
Creación de instancias de variables locales
Se considera que se crea una instancia de una variable local cuando la ejecución entra en el ámbito de la
variable. Por ejemplo, cuando se invoca el método siguiente, x se crea una instancia de la variable local y se
inicializa tres veces, una vez para cada iteración del bucle.
Sin embargo, mover la declaración de x fuera del bucle produce una única creación de instancias de x :
static void F() {
int x;
for (int i = 0; i < 3; i++) {
x = i * 2 + 1;
...
}
}
Cuando no se capturan, no hay forma de observar exactamente la frecuencia con la que se crea una instancia de
una variable local, ya que las duraciones de las instancias no se pueden usar para que cada creación de
instancias use simplemente la misma ubicación de almacenamiento. Sin embargo, cuando una función anónima
captura una variable local, los efectos de la creación de instancias se hacen evidentes.
En el ejemplo
using System;
class Test
{
static D[] F() {
D[] result = new D[3];
for (int i = 0; i < 3; i++) {
int x = i * 2 + 1;
result[i] = () => { Console.WriteLine(x); };
}
return result;
}
genera el resultado:
1
3
5
el resultado es:
5
5
5
Si un bucle for declara una variable de iteración, se considera que esa variable se declara fuera del bucle. Por lo
tanto, si se cambia el ejemplo para capturar la variable de iteración en sí:
3
3
3
Es posible que los delegados de función anónimos compartan algunas variables capturadas pero tengan
instancias independientes de otras. Por ejemplo, si F se cambia a
los tres delegados capturan la misma instancia de x , pero las instancias independientes de y , y el resultado
es:
1 1
2 1
3 1
Las funciones anónimas independientes pueden capturar la misma instancia de una variable externa. En el
ejemplo:
using System;
class Test
{
static void Main() {
int x = 0;
Setter s = (int value) => { x = value; };
Getter g = () => { return x; };
s(5);
Console.WriteLine(g());
s(10);
Console.WriteLine(g());
}
}
las dos funciones anónimas capturan la misma instancia de la variable local x y, por tanto, pueden
"comunicarse" a través de esa variable. La salida del ejemplo es:
5
10
Expresiones de consulta
Las expresiones de consulta proporcionan una sintaxis integrada de lenguaje para las consultas que es
similar a los lenguajes de consulta jerárquica y relacional, como SQL y XQuery.
query_expression
: from_clause query_body
;
from_clause
: 'from' type? identifier 'in' expression
;
query_body
: query_body_clauses? select_or_group_clause query_continuation?
;
query_body_clauses
: query_body_clause
| query_body_clauses query_body_clause
;
query_body_clause
: from_clause
| let_clause
| where_clause
| join_clause
| join_into_clause
| orderby_clause
;
let_clause
: 'let' identifier '=' expression
;
where_clause
: 'where' boolean_expression
;
join_clause
: 'join' type? identifier 'in' expression 'on' expression 'equals' expression
;
join_into_clause
: 'join' type? identifier 'in' expression 'on' expression 'equals' expression 'into' identifier
;
orderby_clause
: 'orderby' orderings
;
orderings
: ordering (',' ordering)*
;
ordering
: expression ordering_direction?
;
ordering_direction
: 'ascending'
| 'descending'
;
select_or_group_clause
: select_clause
| group_clause
;
select_clause
: 'select' expression
;
group_clause
: 'group' expression 'by' expression
;
query_continuation
: 'into' identifier query_body
;
Una expresión de consulta comienza con una from cláusula y termina con una select group cláusula o. La
from cláusula Initial puede ir seguida de cero o más from let cláusulas,, where join o orderby . Cada
from cláusula es un generador que introduce una *variable de rango _ que va por los elementos de una
secuencia _ * * *. Cada let cláusula presenta una variable de rango que representa un valor calculado por
medio de las variables de rango anteriores. Cada where cláusula es un filtro que excluye elementos del
resultado. Cada join cláusula compara las claves especificadas de la secuencia de origen con claves de otra
secuencia, produciendo pares coincidentes. Cada orderby cláusula reordena los elementos según los criterios
especificados. La select cláusula final o group especifica la forma del resultado en términos de las variables
de rango. Por último, into se puede usar una cláusula para "Insertar" consultas tratando los resultados de una
consulta como un generador en una consulta posterior.
Ambigüedades en expresiones de consulta
Las expresiones de consulta contienen una serie de "palabras clave contextuales", es decir, identificadores que
tienen un significado especial en un contexto determinado. En concreto, se trata from de,, where join , on ,
equals , into , let , orderby , ascending ,, descending select group y by . Para evitar ambigüedades en
las expresiones de consulta producidas por el uso mixto de estos identificadores como palabras clave o
nombres simples, estos identificadores se consideran palabras clave cuando se producen en cualquier parte de
una expresión de consulta.
Para este propósito, una expresión de consulta es cualquier expresión que empiece por " from identifier "
seguido de cualquier token excepto " ; ", " = " o " , ".
Para usar estas palabras como identificadores dentro de una expresión de consulta, se les puede anteponer " @
" (identificadores).
Traducción de expresiones de consulta
El lenguaje C# no especifica la semántica de ejecución de las expresiones de consulta. En su lugar, las
expresiones de consulta se convierten en invocaciones de métodos que se adhieren al patrón de expresión de
consulta (el patrón de expresión de consulta). En concreto, las expresiones de consulta se convierten en
invocaciones de métodos denominados Where ,, Select SelectMany , Join , GroupJoin , OrderBy ,
OrderByDescending , ThenBy , ThenByDescending , GroupBy y Cast . Se espera que estos métodos tengan firmas
concretas y tipos de resultado, como se describe en el patrón de expresión de consulta. Estos métodos pueden
ser métodos de instancia del objeto que se consulta o métodos de extensión que son externos al objeto e
implementan la ejecución real de la consulta.
La conversión de las expresiones de consulta a las invocaciones de método es una asignación sintáctica que se
produce antes de que se haya realizado cualquier enlace de tipo o resolución de sobrecarga. Se garantiza que la
conversión es sintácticamente correcta, pero no se garantiza que genere código C# correcto semánticamente.
Después de la traducción de las expresiones de consulta, las invocaciones de método resultantes se procesan
como invocaciones de método regulares y esto puede a su vez detectar errores, por ejemplo, si los métodos no
existen, si los argumentos tienen tipos incorrectos, o si los métodos son genéricos y se produce un error en la
inferencia de tipos.
Una expresión de consulta se procesa mediante la aplicación repetida de las siguientes traducciones hasta que
no se puedan realizar más reducciones. Las traducciones se muestran por orden de aplicación: cada sección
presupone que las traducciones de las secciones anteriores se han realizado de forma exhaustiva y, una vez
agotadas, una sección no se volverá a visitar posteriormente en el procesamiento de la misma expresión de
consulta.
No se permite la asignación a variables de rango en expresiones de consulta. Sin embargo, se permite que una
implementación de C# no siempre aplique esta restricción, ya que a veces esto no es posible con el esquema de
traducción sintáctica que se muestra aquí.
Ciertas traducciones insertan variables de rango con identificadores transparentes indicados por * . Las
propiedades especiales de los identificadores transparentes se tratan en profundidad en los identificadores
transparentes.
Cláusulas SELECT y GroupBy con continuaciones
Expresión de consulta con una continuación
se traduce en
Las traducciones de las secciones siguientes suponen que las consultas no tienen ninguna into continuación.
En el ejemplo
from c in customers
group c by c.Country into g
select new { Country = g.Key, CustCount = g.Count() }
se traduce en
from g in
from c in customers
group c by c.Country
select new { Country = g.Key, CustCount = g.Count() }
from T x in e
se traduce en
join T x in e on k1 equals k2
se traduce en
Las traducciones de las secciones siguientes suponen que las consultas no tienen tipos de variable de intervalo
explícitos.
En el ejemplo
se traduce en
from c in customers.Cast<Customer>()
where c.City == "London"
select c
customers.
Cast<Customer>().
Where(c => c.City == "London")
Los tipos de variables de rango explícitos son útiles para consultar colecciones que implementan la interfaz no
genérica IEnumerable , pero no la IEnumerable<T> interfaz genérica. En el ejemplo anterior, sería el caso si
customers fuera de tipo ArrayList .
from x in e select x
se traduce en
( e ) . Select ( x => x )
En el ejemplo
from c in customers
select c
se traduce en
customers.Select(c => c)
Una expresión de consulta degenerada es aquella que selecciona trivialmente los elementos del origen. Una fase
posterior de la traducción quita las consultas degeneradas que se introdujeron en otros pasos de traducción
mediante su origen. Sin embargo, es importante asegurarse de que el resultado de una expresión de consulta
nunca sea el propio objeto de origen, ya que esto revelaría el tipo e identidad del origen al cliente de la consulta.
Por lo tanto, este paso protege las consultas degeneradas escritas directamente en el código fuente mediante
una llamada explícita Select en el origen. A continuación, llega a los implementadores de Select y otros
operadores de consulta para asegurarse de que estos métodos nunca devuelven el propio objeto de origen.
Cláusulas from, Let, Where, join y OrderBy
Expresión de consulta con una segunda from cláusula seguida de una select cláusula
from x1 in e1
from x2 in e2
select v
se traduce en
Expresión de consulta con una segunda from cláusula seguida de un valor distinto de una select cláusula:
from x1 in e1
from x2 in e2
...
se traduce en
from x in e
let y = f
...
se traduce en
from * in ( e ) . Select ( x => new { x , y = f } )
...
from x in e
where f
...
se traduce en
Expresión de consulta con una join cláusula sin un into seguido de una select cláusula
from x1 in e1
join x2 in e2 on k1 equals k2
select v
se traduce en
Expresión de consulta con una join cláusula sin un into seguido de un elemento distinto de una select
cláusula
from x1 in e1
join x2 in e2 on k1 equals k2
...
se traduce en
Una expresión de consulta con una join cláusula con una into cláusula seguida de una select cláusula
from x1 in e1
join x2 in e2 on k1 equals k2 into g
select v
se traduce en
Una expresión de consulta con una join cláusula con un into seguido de un elemento distinto de una
select cláusula
from x1 in e1
join x2 in e2 on k1 equals k2 into g
...
se traduce en
from x in e
orderby k1 , k2 , ..., kn
...
se traduce en
from x in ( e ) .
OrderBy ( x => k1 ) .
ThenBy ( x => k2 ) .
... .
ThenBy ( x => kn )
...
Si una cláusula de ordenación especifica un descending indicador de dirección, se produce una invocación de
OrderByDescending o ThenByDescending en su lugar.
Las siguientes traducciones suponen que no let hay where join orderby cláusulas, o, ni más de una from
cláusula inicial en cada expresión de consulta.
En el ejemplo
from c in customers
from o in c.Orders
select new { c.Name, o.OrderID, o.Total }
se traduce en
customers.
SelectMany(c => c.Orders,
(c,o) => new { c.Name, o.OrderID, o.Total }
)
En el ejemplo
from c in customers
from o in c.Orders
orderby o.Total descending
select new { c.Name, o.OrderID, o.Total }
se traduce en
from * in customers.
SelectMany(c => c.Orders, (c,o) => new { c, o })
orderby o.Total descending
select new { c.Name, o.OrderID, o.Total }
customers.
SelectMany(c => c.Orders, (c,o) => new { c, o }).
OrderByDescending(x => x.o.Total).
Select(x => new { x.c.Name, x.o.OrderID, x.o.Total })
from o in orders
let t = o.Details.Sum(d => d.UnitPrice * d.Quantity)
where t >= 1000
select new { o.OrderID, Total = t }
se traduce en
from * in orders.
Select(o => new { o, t = o.Details.Sum(d => d.UnitPrice * d.Quantity) })
where t >= 1000
select new { o.OrderID, Total = t }
orders.
Select(o => new { o, t = o.Details.Sum(d => d.UnitPrice * d.Quantity) }).
Where(x => x.t >= 1000).
Select(x => new { x.o.OrderID, Total = x.t })
from c in customers
join o in orders on c.CustomerID equals o.CustomerID
select new { c.Name, o.OrderDate, o.Total }
se traduce en
En el ejemplo
from c in customers
join o in orders on c.CustomerID equals o.CustomerID into co
let n = co.Count()
where n >= 10
select new { c.Name, OrderCount = n }
se traduce en
from * in customers.
GroupJoin(orders, c => c.CustomerID, o => o.CustomerID,
(c, co) => new { c, co })
let n = co.Count()
where n >= 10
select new { c.Name, OrderCount = n }
customers.
GroupJoin(orders, c => c.CustomerID, o => o.CustomerID,
(c, co) => new { c, co }).
Select(x => new { x, n = x.co.Count() }).
Where(y => y.n >= 10).
Select(y => new { y.x.c.Name, OrderCount = y.n)
donde x y y son identificadores generados por el compilador que, de lo contrario, son invisibles e
inaccesibles.
En el ejemplo
from o in orders
orderby o.Customer.Name, o.Total descending
select o
orders.
OrderBy(o => o.Customer.Name).
ThenByDescending(o => o.Total)
Cláusulas Select
Una expresión de consulta con el formato
from x in e select v
se traduce en
( e ) . Select ( x => v )
( e )
Por ejemplo
from c in customers.Where(c => c.City == "London")
select c
simplemente se traduce en
Cláusulas GroupBy
Una expresión de consulta con el formato
from x in e group v by k
se traduce en
( e ) . GroupBy ( x => k )
En el ejemplo
from c in customers
group c.Name by c.Country
se traduce en
customers.
GroupBy(c => c.Country, c => c.Name)
Identificadores transparentes
Ciertas traducciones insertan variables de intervalo con *identificadores transparentes _ indicados por _ .
Los identificadores transparentes no son una característica de lenguaje adecuada; solo existen como un paso
intermedio en el proceso de conversión de expresiones de consulta.
Cuando una traducción de consultas inserta un identificador transparente, los pasos de traducción adicionales
propagan el identificador transparente en funciones anónimas e inicializadores de objeto anónimos. En esos
contextos, los identificadores transparentes tienen el siguiente comportamiento:
Cuando un identificador transparente se produce como un parámetro en una función anónima, los
miembros del tipo anónimo asociado están automáticamente en el ámbito del cuerpo de la función anónima.
Cuando un miembro con un identificador transparente está en el ámbito, los miembros de ese miembro
están también en el ámbito.
Cuando un identificador transparente se produce como un declarador de miembro en un inicializador de
objeto anónimo, introduce un miembro con un identificador transparente.
En los pasos de traducción descritos anteriormente, los identificadores transparentes siempre se introducen
junto con los tipos anónimos, con la intención de capturar varias variables de rango como miembros de un
solo objeto. Una implementación de C# puede usar un mecanismo diferente que los tipos anónimos para
agrupar varias variables de rango. Los siguientes ejemplos de traducción suponen que se usan tipos
anónimos y muestran cómo se pueden traducir los identificadores transparentes.
En el ejemplo
from c in customers
from o in c.Orders
orderby o.Total descending
select new { c.Name, o.Total }
se traduce en
from * in customers.
SelectMany(c => c.Orders, (c,o) => new { c, o })
orderby o.Total descending
select new { c.Name, o.Total }
que se traduce en
customers.
SelectMany(c => c.Orders, (c,o) => new { c, o }).
OrderByDescending(* => o.Total).
Select(* => new { c.Name, o.Total })
customers.
SelectMany(c => c.Orders, (c,o) => new { c, o }).
OrderByDescending(x => x.o.Total).
Select(x => new { x.c.Name, x.o.Total })
from c in customers
join o in orders on c.CustomerID equals o.CustomerID
join d in details on o.OrderID equals d.OrderID
join p in products on d.ProductID equals p.ProductID
select new { c.Name, o.OrderDate, p.ProductName }
se traduce en
from * in customers.
Join(orders, c => c.CustomerID, o => o.CustomerID,
(c, o) => new { c, o })
join d in details on o.OrderID equals d.OrderID
join p in products on d.ProductID equals p.ProductID
select new { c.Name, o.OrderDate, p.ProductName }
customers.
Join(orders, c => c.CustomerID, o => o.CustomerID, (c, o) => new { c, o }).
Join(details, * => o.OrderID, d => d.OrderID, (*, d) => new { *, d }).
Join(products, * => d.ProductID, p => p.ProductID, (*, p) => new { *, p }).
Select(* => new { c.Name, o.OrderDate, p.ProductName })
donde x , y y z son identificadores generados por el compilador que, de lo contrario, son invisibles e
inaccesibles.
Patrón de expresión de consulta
El patrón de expresión de consulta establece un patrón de métodos que los tipos pueden implementar para
admitir expresiones de consulta. Dado que las expresiones de consulta se convierten en invocaciones de método
por medio de una asignación sintáctica, los tipos tienen una gran flexibilidad en cómo implementan el patrón de
expresión de consulta. Por ejemplo, los métodos del patrón se pueden implementar como métodos de instancia
o como métodos de extensión porque los dos tienen la misma sintaxis de invocación y los métodos pueden
solicitar delegados o árboles de expresión porque las funciones anónimas son convertibles a ambos.
A continuación se muestra la forma recomendada de un tipo genérico C<T> que admite el patrón de expresión
de consulta. Se usa un tipo genérico para ilustrar las relaciones apropiadas entre los tipos de parámetro y de
resultado, pero también es posible implementar el patrón para tipos no genéricos.
delegate R Func<T1,R>(T1 arg1);
class C
{
public C<T> Cast<T>();
}
class C<T> : C
{
public C<T> Where(Func<T,bool> predicate);
Los métodos anteriores usan los tipos de delegado genérico Func<T1,R> y Func<T1,T2,R> , pero también
podrían haber usado otros tipos de árbol de delegado o de expresión con las mismas relaciones en los tipos de
parámetro y de resultado.
Observe la relación recomendada entre C<T> y O<T> que garantiza que los ThenBy ThenByDescending
métodos y solo están disponibles en el resultado de OrderBy o OrderByDescending . Observe también la forma
recomendada del resultado de GroupBy --una secuencia de secuencias, donde cada secuencia interna tiene una
propiedad adicional Key .
El System.Linq espacio de nombres proporciona una implementación del patrón de operador de consulta para
cualquier tipo que implemente la System.Collections.Generic.IEnumerable<T> interfaz.
Operadores de asignación
Los operadores de asignación asignan un nuevo valor a una variable, una propiedad, un evento o un elemento
de indexador.
assignment
: unary_expression assignment_operator expression
;
assignment_operator
: '='
| '+='
| '-='
| '*='
| '/='
| '%='
| '&='
| '|='
| '^='
| '<<='
| right_shift_assignment
;
El operando izquierdo de una asignación debe ser una expresión clasificada como una variable, un acceso de
propiedad, un acceso de indexador o un acceso de evento.
El = operador se denomina operador de asignación simple . Asigna el valor del operando derecho a la
variable, propiedad o elemento de indexador proporcionado por el operando izquierdo. Es posible que el
operando izquierdo del operador de asignación simple no sea un acceso a eventos (excepto como se describe
en eventos similares a los de campo). El operador de asignación simple se describe en asignación simple.
Los operadores de asignación distintos del = operador se denominan operadores de asignación
compuesta . Estos operadores realizan la operación indicada en los dos operandos y, a continuación, asignan el
valor resultante a la variable, la propiedad o el elemento indexador proporcionado por el operando izquierdo.
Los operadores de asignación compuesta se describen en asignación compuesta.
Los += -= operadores y con una expresión de acceso a eventos como operando izquierdo se denominan
operadores de asignación de eventos. Ningún otro operador de asignación es válido con un acceso de evento
como operando izquierdo. Los operadores de asignación de eventos se describen en asignación de eventos.
Los operadores de asignación son asociativos a la derecha, lo que significa que las operaciones se agrupan de
derecha a izquierda. Por ejemplo, una expresión con el formato a = b = c se evalúa como a = (b = c) .
Asignación simple
El = operador se denomina operador de asignación simple.
Si el operando izquierdo de una asignación simple tiene el formato E.P o E[Ei] donde E tiene el tipo en
tiempo de compilación dynamic , la asignación está enlazada dinámicamente (enlace dinámico). En este caso, el
tipo en tiempo de compilación de la expresión de asignación es dynamic , y la resolución que se describe a
continuación se realizará en tiempo de ejecución en función del tipo en tiempo de ejecución de E .
En una asignación simple, el operando derecho debe ser una expresión que se pueda convertir implícitamente al
tipo del operando izquierdo. La operación asigna el valor del operando derecho a la variable, la propiedad o el
elemento indexador proporcionado por el operando izquierdo.
El resultado de una expresión de asignación simple es el valor asignado al operando izquierdo. El resultado tiene
el mismo tipo que el operando izquierdo y siempre se clasifica como un valor.
Si el operando izquierdo es una propiedad o un indexador, la propiedad o el indexador deben tener un set
descriptor de acceso. Si no es así, se produce un error en tiempo de enlace.
El procesamiento en tiempo de ejecución de una asignación simple del formulario x = y consta de los
siguientes pasos:
Si xse clasifica como una variable:
x se evalúa para generar la variable.
y se evalúa y, si es necesario, se convierte al tipo de x mediante una conversión implícita
(conversiones implícitas).
Si la variable proporcionada por x es un elemento de matriz de un reference_type, se realiza una
comprobación en tiempo de ejecución para asegurarse de que el valor calculado para y es
compatible con la instancia de la matriz de que x es un elemento. La comprobación se realiza
correctamente si y es null , o si existe una conversión de referencia implícita (conversiones de
referencia implícita) del tipo real de la instancia a la que se hace referencia en y el tipo de elemento
real de la instancia de la matriz que contiene x . De lo contrario, se produce una excepción
System.ArrayTypeMismatchException .
El valor resultante de la evaluación y la conversión de y se almacena en la ubicación especificada por
la evaluación de x .
Si x se clasifica como un acceso de propiedad o indizador:
La expresión de instancia (si x no es static ) y la lista de argumentos (si x es un acceso de
indexador) asociadas a x se evalúan, y los resultados se usan en la set invocación del descriptor de
acceso subsiguiente.
y se evalúa y, si es necesario, se convierte al tipo de x mediante una conversión implícita
(conversiones implícitas).
El set descriptor de acceso de x se invoca con el valor calculado para y como su value
argumento.
Las reglas de covarianza de matriz (covarianza de matriz) permiten que un valor de un tipo de matriz A[] sea
una referencia a una instancia de un tipo de matriz B[] , siempre que exista una conversión de referencia
implícita de B a A . Debido a estas reglas, la asignación a un elemento de matriz de un reference_type
requiere una comprobación en tiempo de ejecución para asegurarse de que el valor que se está asignando es
compatible con la instancia de la matriz. En el ejemplo
oa[0] = null; // Ok
oa[1] = "Hello"; // Ok
oa[2] = new ArrayList(); // ArrayTypeMismatchException
Cuando una propiedad o un indizador declarado en una struct_type es el destino de una asignación, la expresión
de instancia asociada con el acceso a la propiedad o indizador debe estar clasificada como una variable. Si la
expresión de instancia se clasifica como un valor, se produce un error en tiempo de enlace. Debido al acceso a
miembros, la misma regla también se aplica a los campos.
Dadas las declaraciones:
struct Point
{
int x, y;
public int X {
get { return x; }
set { x = value; }
}
public int Y {
get { return y; }
set { y = value; }
}
}
struct Rectangle
{
Point a, b;
public Point A {
get { return a; }
set { a = value; }
}
public Point B {
get { return b; }
set { b = value; }
}
}
en el ejemplo
las asignaciones a p.X , p.Y , r.A y r.B se permiten porque p y r son variables. Sin embargo, en el
ejemplo
byte b = 0;
char ch = '\0';
int i = 0;
b += 1; // Ok
b += 1000; // Error, b = 1000 not permitted
b += i; // Error, b = i not permitted
b += (byte)i; // Ok
la razón intuitiva de cada error es que una asignación simple correspondiente también habría sido un error.
Esto también significa que las operaciones de asignación compuesta admiten operaciones de elevación. En el
ejemplo
int? i = 0;
i += 1; // Ok
Una expresión de asignación de eventos no produce un valor. Por lo tanto, una expresión de asignación de
eventos solo es válida en el contexto de una statement_expression (instrucciones de expresión).
Expression
Una expresión es una non_assignment_expression o una asignación.
expression
: non_assignment_expression
| assignment
;
non_assignment_expression
: conditional_expression
| lambda_expression
| query_expression
;
Expresiones constantes
Una constant_expression es una expresión que se puede evaluar por completo en tiempo de compilación.
constant_expression
: expression
;
Una expresión constante debe ser el null literal o un valor con uno de los siguientes tipos: sbyte , byte ,
short , ushort , int , uint , long , ulong , char , float , double , decimal ,,, bool object string o
cualquier tipo de enumeración. Solo se permiten las siguientes construcciones en expresiones constantes:
Literales (incluido el null literal).
Referencias a const miembros de tipos de clase y estructura.
Referencias a miembros de tipos de enumeración.
Referencias a const parámetros o variables locales
Subexpresiones entre paréntesis, que son expresiones constantes.
Expresiones de conversión, siempre que el tipo de destino sea uno de los tipos enumerados anteriormente.
checked``unchecked expresiones y
Expresiones de valor predeterminado
Expresiones Name
Los + - ! ~ operadores unarios predefinidos,, y.
Los operadores binarios predefinidos,,,,,,,,,,,,,,,, + - * / % << >> & | ^ && || == != < > <=
y >= , siempre que cada operando sea de un tipo enumerado anteriormente.
?: Operador condicional.
class C {
const object i = 5; // error: boxing conversion not permitted
const object str = "hello"; // error: implicit reference conversion
}
la inicialización de i es un error porque se requiere una conversión boxing. La inicialización de Str es un error
porque se requiere una conversión de referencia implícita de un valor distinto de NULL.
Siempre que una expresión cumple los requisitos mencionados anteriormente, la expresión se evalúa en tiempo
de compilación. Esto es así incluso si la expresión es una subexpresión de una expresión mayor que contiene
construcciones que no son constantes.
La evaluación en tiempo de compilación de las expresiones constantes utiliza las mismas reglas que la
evaluación en tiempo de ejecución de expresiones no constantes, salvo que, en el caso de que la evaluación en
tiempo de ejecución hubiera producido una excepción, la evaluación en tiempo de compilación provoca un error
en tiempo de compilación.
A menos que una expresión constante se coloque explícitamente en un unchecked contexto, los
desbordamientos que se producen en operaciones aritméticas de tipo entero y en las conversiones durante la
evaluación en tiempo de compilación de la expresión siempre producen errores en tiempo de compilación
(expresiones constantes).
Las expresiones constantes se producen en los contextos que se enumeran a continuación. En estos contextos,
se produce un error en tiempo de compilación si una expresión no se puede evaluar por completo en tiempo de
compilación.
Declaraciones de constantes (constantes).
Declaraciones de miembros de enumeración (miembros de enumeración).
Argumentos predeterminados de las listas de parámetros formales (parámetros de método)
case Etiquetas de una switch instrucción (la instrucción switch).
goto case instrucciones (instrucción Goto).
Longitudes de dimensión en una expresión de creación de matriz (expresiones de creación de matrices) que
incluye un inicializador.
Attributes (atributos).
Una conversión de expresión constante implícita (conversiones de expresión constante implícita) permite
convertir una expresión constante de tipo en int sbyte ,, byte , short ushort , uint o ulong , siempre
que el valor de la expresión constante esté dentro del intervalo del tipo de destino.
Expresiones booleanas
Un Boolean_expression es una expresión que produce un resultado de tipo bool , ya sea directamente o a
través de operator true la aplicación de en determinados contextos, tal y como se especifica en el siguiente.
boolean_expression
: expression
;
La expresión condicional de control de una if_statement (la instrucción if), while_statement (la instrucción while)
, do_statement (la instrucción do) o for_Statement (la instrucción for) es una Boolean_expression. La expresión
condicional de control del ?: operador (operador condicional) sigue las mismas reglas que una
Boolean_expression, pero por razones de precedencia de operadores se clasifica como
conditional_or_expression.
Un Boolean_expression E es necesario para poder generar un valor de tipo bool , como se indica a
continuación:
Si E se pueden convertir implícitamente a bool , en tiempo de ejecución, se aplica la conversión implícita.
De lo contrario, se utiliza la resolución de sobrecargas de operador unario (resolución de sobrecarga de
operadores unarios) para encontrar una implementación óptima única de operador true en E y esa
implementación se aplica en tiempo de ejecución.
Si no se encuentra ningún operador de este tipo, se produce un error en tiempo de enlace.
El tipo de DBBool estructura de tipo Boolean de base de datos proporciona un ejemplo de un tipo que
implementa operator true y operator false .
Instrucciones
18/09/2021 • 107 minutes to read
C# proporciona diversas instrucciones. La mayoría de estas instrucciones serán familiares para los
desarrolladores que han programado en C y C++.
statement
: labeled_statement
| declaration_statement
| embedded_statement
;
embedded_statement
: block
| empty_statement
| expression_statement
| selection_statement
| iteration_statement
| jump_statement
| try_statement
| checked_statement
| unchecked_statement
| lock_statement
| using_statement
| yield_statement
| embedded_statement_unsafe
;
El embedded_statement nonterminal se utiliza para las instrucciones que aparecen dentro de otras
instrucciones. El uso de embedded_statement en lugar de Statement excluye el uso de las instrucciones de
declaración y las instrucciones con etiquetas en estos contextos. En el ejemplo
void F(bool b) {
if (b)
int i = 44;
}
genera un error en tiempo de compilación porque una if instrucción requiere una embedded_statement en
lugar de una instrucción para su bifurcación if. Si se permitía este código, la variable i se declararía, pero
nunca se podía usar. Tenga en cuenta, sin embargo, que si coloca i la declaración de en un bloque, el ejemplo
es válido.
void F() {
const int i = 1;
if (i == 2) Console.WriteLine("unreachable");
}
la expresión booleana de la if instrucción es una expresión constante porque ambos operandos del ==
operador son constantes. Como la expresión constante se evalúa en tiempo de compilación, lo que genera el
valor false , Console.WriteLine se considera que la invocación no es accesible. Sin embargo, si i se cambia
para ser una variable local
void F() {
int i = 1;
if (i == 2) Console.WriteLine("reachable");
}
void F(int x) {
Console.WriteLine("start");
if (x < 0) Console.WriteLine("negative");
}
Hay dos situaciones en las que se trata de un error en tiempo de compilación para que el punto final de una
instrucción sea alcanzable:
Dado que la switch instrucción no permite que una sección switch pase a la siguiente sección switch, se
trata de un error en tiempo de compilación para que el punto final de la lista de instrucciones de una sección
switch sea accesible. Si se produce este error, normalmente es una indicación de que break falta una
instrucción.
Es un error en tiempo de compilación el punto final del bloque de un miembro de función que calcula un
valor al que se puede tener acceso. Si se produce este error, normalmente es una indicación de que return
falta una instrucción.
Blocks
Un bloque permite que se escriban varias instrucciones en contextos donde se permite una única instrucción.
block
: '{' statement_list? '}'
;
Un bloque consta de un statement_list opcional (listas de instrucciones), entre llaves. Si se omite la lista de
instrucciones, se dice que el bloque está vacío.
Un bloque puede contener instrucciones de declaración (instrucciones de declaración). El ámbito de una variable
local o constante declarada en un bloque es el bloque.
Un bloque se ejecuta de la siguiente manera:
Si el bloque está vacío, el control se transfiere al punto final del bloque.
Si el bloque no está vacío, el control se transfiere a la lista de instrucciones. Cuando y si el control alcanza el
punto final de la lista de instrucciones, el control se transfiere al punto final del bloque.
La lista de instrucciones de un bloque es accesible si el propio bloque es accesible.
El punto final de un bloque es accesible si el bloque está vacío o si el punto final de la lista de instrucciones es
accesible.
Un bloque que contiene una o más yield instrucciones (la instrucción yield) se denomina bloque de iteradores.
Los bloques de iterador se usan para implementar miembros de función como iteradores (iteradores). Se
aplican algunas restricciones adicionales a los bloques de iteradores:
Se trata de un error en tiempo de compilación para return que una instrucción aparezca en un bloque de
iteradores (pero yield return se permiten las instrucciones).
Es un error en tiempo de compilación que un bloque de iteradores contenga un contexto no seguro
(contextos no seguros). Un bloque de iterador siempre define un contexto seguro, incluso cuando su
declaración está anidada en un contexto no seguro.
Listas de instrucciones
Una *lista de instrucciones _ consta de una o varias instrucciones escritas en secuencia. Las listas de
instrucciones se producen en _block * s (bloques) y en Switch_block s (la instrucción switch).
statement_list
: statement+
;
Una lista de instrucciones se ejecuta mediante la transferencia de control a la primera instrucción. Cuando y si el
control alcanza el punto final de una instrucción, el control se transfiere a la siguiente instrucción. Cuando el
control alcanza el punto final de la última instrucción, se transfiere el control al punto final de la lista de
instrucciones.
Se puede tener acceso a una instrucción de una lista de instrucciones si se cumple al menos una de las
siguientes condiciones:
La instrucción es la primera instrucción y la propia lista de instrucciones es accesible.
El punto final de la instrucción anterior es accesible.
La instrucción es una instrucción con etiqueta y una instrucción accesible hace referencia a la etiqueta goto .
El punto final de una lista de instrucciones es accesible si el punto final de la última instrucción de la lista es
accesible.
Instrucción vacía
Un empty_statement no hace nada.
empty_statement
: ';'
;
Se utiliza una instrucción vacía cuando no hay ninguna operación que realizar en un contexto donde se requiere
una instrucción.
La ejecución de una instrucción vacía simplemente transfiere el control al punto final de la instrucción. Por lo
tanto, el punto final de una instrucción vacía es accesible si la instrucción vacía es accesible.
Se puede usar una instrucción vacía al escribir una while instrucción con un cuerpo nulo:
void ProcessMessages() {
while (ProcessMessage())
;
}
Además, se puede usar una instrucción vacía para declarar una etiqueta justo antes del cierre " } " de un
bloque:
void F() {
...
if (done) goto exit;
...
exit: ;
}
labeled_statement
: identifier ':' statement
;
Una instrucción con etiqueta declara una etiqueta con el nombre proporcionado por el identificador. El ámbito
de una etiqueta es el bloque entero en el que se declara la etiqueta, incluidos los bloques anidados. Es un error
en tiempo de compilación que dos etiquetas con el mismo nombre tienen ámbitos superpuestos.
Se puede hacer referencia a una etiqueta desde goto instrucciones (instrucción Goto) dentro del ámbito de la
etiqueta. Esto significa que las goto instrucciones pueden transferir el control dentro de los bloques y fuera de
los bloques, pero nunca a los bloques.
Las etiquetas tienen su propio espacio de declaración y no interfieren con otros identificadores. En el ejemplo
int F(int x) {
if (x >= 0) goto x;
x = -x;
x: return x;
}
Instrucciones de declaración
Un declaration_statement declara una variable o constante local. Las instrucciones de declaración se permiten
en bloques, pero no se permiten como instrucciones incrustadas.
declaration_statement
: local_variable_declaration ';'
| local_constant_declaration ';'
;
local_variable_type
: type
| 'var'
;
local_variable_declarators
: local_variable_declarator
| local_variable_declarators ',' local_variable_declarator
;
local_variable_declarator
: identifier
| identifier '=' local_variable_initializer
;
local_variable_initializer
: expression
| array_initializer
| local_variable_initializer_unsafe
;
El valor de una variable local se obtiene en una expresión usando un simple_name (nombres simples) y el valor
de una variable local se modifica mediante una asignación (operadores de asignación). Una variable local debe
estar asignada definitivamente (asignación definitiva) en cada ubicación donde se obtiene su valor.
El ámbito de una variable local declarada en un local_variable_declaration es el bloque en el que se produce la
declaración. Es un error hacer referencia a una variable local en una posición textual que precede al
local_variable_declarator de la variable local. Dentro del ámbito de una variable local, se trata de un error en
tiempo de compilación para declarar otra variable o constante local con el mismo nombre.
Una declaración de variable local que declara varias variables es equivalente a varias declaraciones de variables
únicas con el mismo tipo. Además, un inicializador de variable en una declaración de variable local se
corresponde exactamente con una instrucción de asignación que se inserta inmediatamente después de la
declaración.
En el ejemplo
void F() {
int x = 1, y, z = x * 2;
}
corresponde exactamente a
void F() {
int x; x = 1;
int y;
int z; z = x * 2;
}
En una declaración de variable local con tipo implícito, el tipo de la variable local que se está declarando se toma
como el mismo tipo de la expresión que se usa para inicializar la variable. Por ejemplo:
var i = 5;
var s = "Hello";
var d = 1.0;
var numbers = new int[] {1, 2, 3};
var orders = new Dictionary<int,Order>();
Las declaraciones de variables locales con tipo implícito anterior son exactamente equivalentes a las siguientes
declaraciones con tipo explícito:
int i = 5;
string s = "Hello";
double d = 1.0;
int[] numbers = new int[] {1, 2, 3};
Dictionary<int,Order> orders = new Dictionary<int,Order>();
local_constant_declaration
: 'const' type constant_declarators
;
constant_declarators
: constant_declarator (',' constant_declarator)*
;
constant_declarator
: identifier '=' constant_expression
;
Instrucciones de expresión
Un expression_statement evalúa una expresión determinada. El valor calculado por la expresión, si existe, se
descarta.
expression_statement
: statement_expression ';'
;
statement_expression
: invocation_expression
| null_conditional_invocation_expression
| object_creation_expression
| assignment
| post_increment_expression
| post_decrement_expression
| pre_increment_expression
| pre_decrement_expression
| await_expression
;
No todas las expresiones se permiten como instrucciones. En concreto, las expresiones como x + y y x == 1
que simplemente calculan un valor (que se descartarán) no se permiten como instrucciones.
La ejecución de una expression_statement evalúa la expresión contenida y, a continuación, transfiere el control al
punto final de la expression_statement. El punto final de una expression_statement es accesible si ese
expression_statement es accesible.
Instrucciones de selección
Las instrucciones de selección seleccionan una de varias instrucciones posibles para su ejecución según el valor
de alguna expresión.
selection_statement
: if_statement
| switch_statement
;
Instrucción If
La if instrucción selecciona una instrucción para su ejecución basada en el valor de una expresión booleana.
if_statement
: 'if' '(' boolean_expression ')' embedded_statement
| 'if' '(' boolean_expression ')' embedded_statement 'else' embedded_statement
;
Un else elemento está asociado a la parte anterior léxicamente más cercana if permitida por la sintaxis. Por
lo tanto, una if instrucción con el formato
es equivalente a
if (x) {
if (y) {
F();
}
else {
G();
}
}
La instrucción switch
La instrucción switch selecciona la ejecución de una lista de instrucciones que tiene una etiqueta de conmutador
asociada que corresponde al valor de la expresión switch.
switch_statement
: 'switch' '(' expression ')' switch_block
;
switch_block
: '{' switch_section* '}'
;
switch_section
: switch_label+ statement_list
;
switch_label
: 'case' constant_expression ':'
| 'default' ':'
;
Un switch_statement se compone de la palabra clave switch , seguida de una expresión entre paréntesis
(denominada expresión switch), seguida de un switch_block. El switch_block consta de cero o más
switch_section s, entre llaves. Cada switch_section consta de uno o varios switch_label s seguidos de un
statement_list (listas de instrucciones).
El tipo de control de una switch instrucción se establece mediante la expresión switch.
Si el tipo de la expresión switch es sbyte , byte , short , ushort , int , uint , long , ulong , bool ,
char , string o un enum_type, o si es el tipo que acepta valores NULL correspondiente a uno de estos
tipos, es el tipo de control de la switch instrucción.
De lo contrario, debe existir exactamente una conversión implícita definida por el usuario (conversiones
definidas por el usuario) del tipo de la expresión switch a uno de los siguientes tipos de control posibles:
sbyte , byte , short , ushort , int , uint , long , ulong ,, char string o, un tipo que acepta valores
NULL correspondiente a uno de esos tipos.
De lo contrario, si no existe ninguna conversión implícita, o si existe más de una conversión implícita de este
tipo, se produce un error en tiempo de compilación.
La expresión constante de cada case etiqueta debe indicar un valor que se pueda convertir implícitamente
(conversiones implícitas) en el tipo aplicable de la switch instrucción. Se produce un error en tiempo de
compilación si dos o más case etiquetas de la misma switch instrucción especifican el mismo valor constante.
Puede haber como máximo una default etiqueta en una instrucción switch.
Una switch instrucción se ejecuta de la siguiente manera:
La expresión switch se evalúa y se convierte al tipo aplicable.
Si una de las constantes especificadas en una case etiqueta en la misma switch instrucción es igual al valor
de la expresión switch, el control se transfiere a la lista de instrucciones que sigue a la case etiqueta
coincidente.
Si ninguna de las constantes especificadas en case las etiquetas de la misma switch instrucción es igual al
valor de la expresión switch y, si hay una default etiqueta, el control se transfiere a la lista de instrucciones
que sigue a la default etiqueta.
Si ninguna de las constantes especificadas en case las etiquetas de la misma switch instrucción es igual al
valor de la expresión switch y no hay ninguna default etiqueta, el control se transfiere al punto final de la
switch instrucción.
Si el punto final de la lista de instrucciones de una sección switch es accesible, se producirá un error en tiempo
de compilación. Esto se conoce como la regla "no pasar de un paso". En el ejemplo
switch (i) {
case 0:
CaseZero();
break;
case 1:
CaseOne();
break;
default:
CaseOthers();
break;
}
es válido porque ninguna sección del modificador tiene un punto final alcanzable. A diferencia de C y C++, no se
permite la ejecución de una sección switch a la siguiente sección switch y el ejemplo
switch (i) {
case 0:
CaseZero();
case 1:
CaseZeroOrOne();
default:
CaseAny();
}
produce un error en tiempo de compilación. Cuando la ejecución de una sección switch va seguida de la
ejecución de otra sección switch, goto case goto default se debe usar una instrucción or explícita:
switch (i) {
case 0:
CaseZero();
goto case 1;
case 1:
CaseZeroOrOne();
goto default;
default:
CaseAny();
break;
}
switch (i) {
case 0:
CaseZero();
break;
case 1:
CaseOne();
break;
case 2:
default:
CaseTwo();
break;
}
es válido. En el ejemplo no se infringe la regla "no hay que pasar" porque las etiquetas case 2: y default:
forman parte del mismo switch_section.
La regla "no pasar" evita una clase común de errores que se producen en C y C++ cuando las break
instrucciones se omiten accidentalmente. Además, debido a esta regla, las secciones switch de una switch
instrucción se pueden reorganizar arbitrariamente sin afectar al comportamiento de la instrucción. Por ejemplo,
las secciones de la switch instrucción anterior se pueden revertir sin afectar al comportamiento de la
instrucción:
switch (i) {
default:
CaseAny();
break;
case 1:
CaseZeroOrOne();
goto default;
case 0:
CaseZero();
goto case 1;
}
La lista de instrucciones de una sección switch normalmente finaliza en break una goto case instrucción, o
goto default , pero se permite cualquier construcción que representa el punto final de la lista de instrucciones
inaccesible. Por ejemplo, while se sabe que una instrucción controlada por la expresión booleana true nunca
alcanza su punto final. Del mismo modo, una throw return instrucción o siempre transfiere el control en otro
lugar y nunca alcanza su punto final. Por lo tanto, el ejemplo siguiente es válido:
switch (i) {
case 0:
while (true) F();
case 1:
throw new ArgumentException();
case 2:
return;
}
El tipo de control de una switch instrucción puede ser el tipo string . Por ejemplo:
Al igual que los operadores de igualdad de cadena (operadores de igualdad de cadena), la instrucción distingue
switch entre mayúsculas y minúsculas y ejecutará una sección de modificador determinada solo si la cadena
de expresión de modificador coincide exactamente con una case constante de etiqueta.
Cuando el tipo de control de una switch instrucción es string , null se permite el valor como una constante
de etiqueta de caso.
Los statement_list s de un switch_block pueden contener instrucciones de declaración (instrucciones de
declaración). El ámbito de una variable local o constante declarada en un bloque switch es el bloque switch.
La lista de instrucciones de una sección de modificador determinada es accesible si la switch instrucción es
accesible y se cumple al menos una de las siguientes condiciones:
La expresión switch es un valor que no es constante.
La expresión switch es un valor constante que coincide con una case etiqueta en la sección switch.
La expresión switch es un valor constante que no coincide con ninguna case etiqueta y la sección switch
contiene la default etiqueta.
Se hace referencia a una etiqueta switch de la sección switch mediante una goto case instrucción o
alcanzable goto default .
El punto final de una switch instrucción es accesible si se cumple al menos una de las siguientes condiciones:
La switch instrucción contiene una instrucción accesible break que sale de la switch instrucción.
La switch instrucción es accesible, la expresión switch es un valor no constante y no hay ninguna default
etiqueta.
La switch instrucción es accesible, la expresión switch es un valor constante que no coincide con ninguna
case etiqueta y no hay ninguna default etiqueta.
Instrucciones de iteración
Las instrucciones de iteración ejecutan repetidamente una instrucción incrustada.
iteration_statement
: while_statement
| do_statement
| for_statement
| foreach_statement
;
La instrucción while
La while instrucción ejecuta condicionalmente una instrucción incrustada cero o más veces.
while_statement
: 'while' '(' boolean_expression ')' embedded_statement
;
do_statement
: 'do' embedded_statement 'while' '(' boolean_expression ')' ';'
;
Dentro de la instrucción incrustada de una do instrucción, break se puede usar una instrucción (la instrucción
break) para transferir el control al punto final de la do instrucción (por lo tanto, finalizar la iteración de la
instrucción incrustada) y continue se puede usar una instrucción (la instrucción continue) para transferir el
control al punto final de la instrucción incrustada.
La instrucción insertada de una do instrucción es accesible si la do instrucción es accesible.
El punto final de una do instrucción es accesible si se cumple al menos una de las siguientes condiciones:
La do instrucción contiene una instrucción accesible break que sale de la do instrucción.
El punto final de la instrucción incrustada es accesible y la expresión booleana no tiene el valor constante
true .
La instrucción for
La for instrucción evalúa una secuencia de expresiones de inicialización y, a continuación, mientras una
condición es true, ejecuta repetidamente una instrucción incrustada y evalúa una secuencia de expresiones de
iteración.
for_statement
: 'for' '(' for_initializer? ';' for_condition? ';' for_iterator? ')' embedded_statement
;
for_initializer
: local_variable_declaration
| statement_expression_list
;
for_condition
: boolean_expression
;
for_iterator
: statement_expression_list
;
statement_expression_list
: statement_expression (',' statement_expression)*
;
Dentro de la instrucción incrustada de una for instrucción, break se puede usar una instrucción (la instrucción
break) para transferir el control al punto final de la for instrucción (con lo que finaliza la iteración de la
instrucción incrustada) y continue se puede usar una instrucción (la instrucción continue) para transferir el
control al punto final de la instrucción incrustada (de modo que se ejecute el for_iterator y realice otra iteración
de la for instrucción, empezando por el for_condition)
La instrucción insertada de una for instrucción es accesible si se cumple una de las siguientes condiciones:
La for instrucción es accesible y no hay ningún for_condition presente.
La for instrucción es accesible y hay un for_condition presente y no tiene el valor constante false .
El punto final de una for instrucción es accesible si se cumple al menos una de las siguientes condiciones:
La for instrucción contiene una instrucción accesible break que sale de la for instrucción.
La for instrucción es accesible y hay un for_condition presente y no tiene el valor constante true .
La instrucción foreach
La foreach instrucción enumera los elementos de una colección y ejecuta una instrucción incrustada para cada
elemento de la colección.
foreach_statement
: 'foreach' '(' local_variable_type identifier 'in' expression ')' embedded_statement
;
foreach (V v in x) embedded_statement
{
E e = ((C)(x)).GetEnumerator();
try {
while (e.MoveNext()) {
V v = (V)(T)e.Current;
embedded_statement
}
}
finally {
... // Dispose e
}
}
La variable e no es visible o accesible para la expresión x o la instrucción incrustada ni cualquier otro código
fuente del programa. La variable v es de solo lectura en la instrucción insertada. Si no hay una conversión
explícita (conversiones explícitas) de T (el tipo de elemento) en V (el local_variable_type en la instrucción
foreach), se genera un error y no se realiza ningún paso más. Si x tiene el valor null ,
System.NullReferenceException se produce una excepción en tiempo de ejecución.
Una implementación puede implementar una instrucción foreach determinada de forma diferente, por ejemplo,
por motivos de rendimiento, siempre que el comportamiento sea coherente con la expansión anterior.
La colocación de v dentro del bucle while es importante para que lo Capture cualquier función anónima que se
produzca en el embedded_statement.
Por ejemplo:
int[] values = { 7, 9, 13 };
Action f = null;
f();
Si v se declaró fuera del bucle while, se compartirá entre todas las iteraciones y su valor después del bucle for
sería el valor final, 13 , que es lo que la invocación de f imprimiría. En su lugar, dado que cada iteración tiene
su propia variable v , la f que captura en la primera iteración seguirá conservando el valor 7 , que es lo que
se va a imprimir. (Nota: versiones anteriores de C# declaradas v fuera del bucle while).
El cuerpo del bloque finally se construye según los pasos siguientes:
Si hay una conversión implícita de E en la System.IDisposable interfaz,
Si E es un tipo de valor que no acepta valores NULL, la cláusula finally se expande al equivalente
semántico de:
finally {
((System.IDisposable)e).Dispose();
}
finally {
if (e != null) ((System.IDisposable)e).Dispose();
}
salvo que si E es un tipo de valor o un parámetro de tipo al que se ha creado una instancia de un tipo de
valor, la conversión de e a System.IDisposable no hará que se produzca la conversión boxing.
De lo contrario, si E es un tipo sellado, la cláusula finally se expande a un bloque vacío:
finally {
}
finally {
System.IDisposable d = e as System.IDisposable;
if (d != null) d.Dispose();
}
La variable local d no es visible o accesible para cualquier código de usuario. En concreto, no entra en
conflicto con ninguna otra variable cuyo ámbito incluya el bloque Finally.
El orden en el que se foreach recorren los elementos de una matriz es el siguiente: en el caso de las matrices
unidimensionales, los elementos se recorren en orden creciente de índice, empezando por el índice 0 y
terminando por el índice Length - 1 . En el caso de las matrices multidimensionales, los elementos se recorren
de forma que los índices de la dimensión situada más a la derecha aumenten primero, la dimensión izquierda
siguiente y así sucesivamente hacia la izquierda.
En el ejemplo siguiente se imprime cada valor en una matriz bidimensional, en el orden de los elementos:
using System;
class Test
{
static void Main() {
double[,] values = {
{1.2, 2.3, 3.4, 4.5},
{5.6, 6.7, 7.8, 8.9}
};
Console.WriteLine();
}
}
En el ejemplo
int[] numbers = { 1, 3, 5, 7, 9 };
foreach (var n in numbers) Console.WriteLine(n);
Instrucciones de salto
Las instrucciones de salto transfieren el control incondicionalmente.
jump_statement
: break_statement
| continue_statement
| goto_statement
| return_statement
| throw_statement
;
La ubicación a la que una instrucción de salto transfiere el control se denomina destino de la instrucción de
salto.
Cuando una instrucción de salto se produce dentro de un bloque y el destino de la instrucción de salto está
fuera del bloque, se dice que la instrucción de salto sale del bloque. Aunque una instrucción de salto puede
transferir el control fuera de un bloque, nunca puede transferir el control a un bloque.
La ejecución de instrucciones de salto es complicada por la presencia de instrucciones intermedias try . En
ausencia de estas try instrucciones, una instrucción de salto transfiere incondicionalmente el control de la
instrucción de salto a su destino. En presencia de dichas instrucciones intermedias try , la ejecución es más
compleja. Si la instrucción de salto sale de uno o más try bloques con finally bloques asociados, el control
se transfiere inicialmente al finally bloque de la try instrucción más interna. Cuando y si el control alcanza el
punto final de un finally bloque, el control se transfiere al finally bloque de la siguiente instrucción de
inclusión try . Este proceso se repite hasta que finally se hayan ejecutado los bloques de todas las
instrucciones que intervienen try .
En el ejemplo
using System;
class Test
{
static void Main() {
while (true) {
try {
try {
Console.WriteLine("Before break");
break;
}
finally {
Console.WriteLine("Innermost finally block");
}
}
finally {
Console.WriteLine("Outermost finally block");
}
}
Console.WriteLine("After break");
}
}
los finally bloques asociados a dos try instrucciones se ejecutan antes de que el control se transfiera al
destino de la instrucción de salto.
La salida generada es la siguiente:
Before break
Innermost finally block
Outermost finally block
After break
Instrucción break
La break instrucción sale de la instrucción de inclusión,,, o más cercana switch while do for foreach .
break_statement
: 'break' ';'
;
El destino de una instrucción es el punto final de la switch instrucción envolvente,,, o más cercana
break
while do for foreach . Si una break instrucción no está delimitada por switch una while instrucción,,
do , for o foreach , se produce un error en tiempo de compilación.
Cuando varias switch instrucciones,,, while do for o foreach se anidan entre sí, una break instrucción
solo se aplica a la instrucción más interna. Para transferir el control entre varios niveles de anidamiento, goto
se debe usar una instrucción (instrucción Goto).
Una break instrucción no puede salir de un finally bloque (la instrucción try). Cuando una break instrucción
aparece dentro de un finally bloque, el destino de la break instrucción debe estar dentro del mismo finally
bloque; de lo contrario, se produce un error en tiempo de compilación.
Una break instrucción se ejecuta de la siguiente manera:
Si la break instrucción sale de uno o más try bloques con finally bloques asociados, el control se
transfiere inicialmente al finally bloque de la instrucción más interna try . Cuando y si el control alcanza
el punto final de un finally bloque, el control se transfiere al finally bloque de la siguiente instrucción de
inclusión try . Este proceso se repite hasta que finally se hayan ejecutado los bloques de todas las
instrucciones que intervienen try .
El control se transfiere al destino de la break instrucción.
Dado break que una instrucción transfiere el control incondicionalmente a otra parte, el punto final de una
break instrucción nunca es accesible.
La instrucción continue
La continue instrucción inicia una nueva iteración de la instrucción envolvente while , do , for o más
cercana foreach .
continue_statement
: 'continue' ';'
;
El destino de una continue instrucción es el punto final de la instrucción incrustada de la instrucción envolvente
while , do , for o más cercana foreach . Si una continue instrucción no está delimitada por while una do
instrucción,, for o foreach , se produce un error en tiempo de compilación.
Cuando varias while do instrucciones,, for o foreach se anidan entre sí, una continue instrucción solo se
aplica a la instrucción más interna. Para transferir el control entre varios niveles de anidamiento, goto se debe
usar una instrucción (instrucción Goto).
Una continue instrucción no puede salir de un finally bloque (la instrucción try). Cuando una continue
instrucción aparece dentro de un finally bloque, el destino de la continue instrucción debe estar dentro del
mismo finally bloque; de lo contrario, se produce un error en tiempo de compilación.
Una continue instrucción se ejecuta de la siguiente manera:
Si la continue instrucción sale de uno o más try bloques con finally bloques asociados, el control se
transfiere inicialmente al finally bloque de la instrucción más interna try . Cuando y si el control alcanza
el punto final de un finally bloque, el control se transfiere al finally bloque de la siguiente instrucción de
inclusión try . Este proceso se repite hasta que finally se hayan ejecutado los bloques de todas las
instrucciones que intervienen try .
El control se transfiere al destino de la continue instrucción.
Dado continue que una instrucción transfiere el control incondicionalmente a otra parte, el punto final de una
continue instrucción nunca es accesible.
La instrucción goto
La goto instrucción transfiere el control a una instrucción marcada por una etiqueta.
goto_statement
: 'goto' identifier ';'
| 'goto' 'case' constant_expression ';'
| 'goto' 'default' ';'
;
El destino de una goto instrucción Identifier es la instrucción con etiqueta con la etiqueta especificada. Si no
existe una etiqueta con el nombre especificado en el miembro de función actual, o si la goto instrucción no está
dentro del ámbito de la etiqueta, se produce un error en tiempo de compilación. Esta regla permite el uso de
una goto instrucción para transferir el control fuera de un ámbito anidado, pero no a un ámbito anidado. En el
ejemplo
using System;
class Test
{
static void Main(string[] args) {
string[,] table = {
{"Red", "Blue", "Green"},
{"Monday", "Wednesday", "Friday"}
};
una goto instrucción se usa para transferir el control fuera de un ámbito anidado.
El destino de una goto case instrucción es la lista de instrucciones de la instrucción de inclusión inmediata
switch (la instrucción switch), que contiene una case etiqueta con el valor constante especificado. Si la
goto case instrucción no está delimitada por una switch instrucción, si el constant_expression no es
implícitamente convertible (conversiones implícitas) al tipo aplicable de la instrucción de inclusión más cercana
switch , o si la instrucción de inclusión más cercana no switch contiene una case etiqueta con el valor
constante especificado, se produce un error en tiempo de compilación.
El destino de una goto default instrucción es la lista de instrucciones de la instrucción de inclusión inmediata
switch (la instrucción switch), que contiene una default etiqueta. Si la goto default instrucción no está
delimitada por una switch instrucción, o si la instrucción de inclusión más cercana no switch contiene una
default etiqueta, se produce un error en tiempo de compilación.
Una goto instrucción no puede salir de un finally bloque (la instrucción try). Cuando una goto instrucción
aparece dentro de un finally bloque, el destino de la goto instrucción debe estar dentro del mismo finally
bloque o, de lo contrario, se producirá un error en tiempo de compilación.
Una goto instrucción se ejecuta de la siguiente manera:
Si la goto instrucción sale de uno o más try bloques con finally bloques asociados, el control se
transfiere inicialmente al finally bloque de la instrucción más interna try . Cuando y si el control alcanza
el punto final de un finally bloque, el control se transfiere al finally bloque de la siguiente instrucción de
inclusión try . Este proceso se repite hasta que finally se hayan ejecutado los bloques de todas las
instrucciones que intervienen try .
El control se transfiere al destino de la goto instrucción.
Dado goto que una instrucción transfiere el control incondicionalmente a otra parte, el punto final de una
goto instrucción nunca es accesible.
La instrucción return
La return instrucción devuelve el control al llamador actual de la función en la que return aparece la
instrucción.
return_statement
: 'return' expression? ';'
;
Una return instrucción sin expresión solo se puede usar en un miembro de función que no calcule un valor, es
decir, un método con el tipo de resultado (cuerpo del método) void , el set descriptor de acceso de una
propiedad o un indizador, y descriptores add remove de acceso de un evento, un constructor de instancia, un
constructor estático o un destructor.
Una return instrucción con una expresión solo se puede usar en un miembro de función que calcula un valor,
es decir, un método con un tipo de resultado no void, el get descriptor de acceso de una propiedad o un
indizador, o un operador definido por el usuario. Debe existir una conversión implícita (conversiones implícitas)
del tipo de la expresión al tipo de valor devuelto del miembro de función contenedora.
Las instrucciones Return también se pueden utilizar en el cuerpo de expresiones de función anónimas
(expresiones de función anónimas) y participar en la determinación de las conversiones que existen para esas
funciones.
Se trata de un error en tiempo de compilación para return que una instrucción aparezca en un finally
bloque (la instrucción try).
Una return instrucción se ejecuta de la siguiente manera:
Si la return instrucción especifica una expresión, se evalúa la expresión y el valor resultante se convierte al
tipo de valor devuelto de la función que la contiene mediante una conversión implícita. El resultado de la
conversión se convierte en el valor de resultado producido por la función.
Si la return instrucción está incluida en uno o varios try catch bloques o con finally bloques
asociados, el control se transfiere inicialmente al finally bloque de la instrucción más interna try .
Cuando y si el control alcanza el punto final de un finally bloque, el control se transfiere al finally
bloque de la siguiente instrucción de inclusión try . Este proceso se repite hasta que finally se hayan
ejecutado los bloques de todas las instrucciones envolventes try .
Si la función contenedora no es una función asincrónica, el control se devuelve al autor de la llamada de la
función contenedora junto con el valor del resultado, si existe.
Si la función contenedora es una función asincrónica, el control se devuelve al llamador actual y el valor del
resultado, si existe, se registra en la tarea devuelta tal y como se describe en (interfaces del enumerador).
Dado return que una instrucción transfiere el control incondicionalmente a otra parte, el punto final de una
return instrucción nunca es accesible.
La instrucción throw
La instrucción throw genera una excepción.
throw_statement
: 'throw' expression? ';'
;
Una throw instrucción con una expresión inicia el valor generado al evaluar la expresión. La expresión debe
indicar un valor del tipo de clase System.Exception , de un tipo de clase que se deriva de System.Exception o de
un tipo de parámetro de tipo que tiene System.Exception (o una subclase de ella) como su clase base efectiva. Si
la evaluación de la expresión produce null , System.NullReferenceException se produce una excepción en su
lugar.
Una throw instrucción sin expresión solo se puede usar en un catch bloque, en cuyo caso la instrucción vuelve
a iniciar la excepción que está controlando actualmente ese catch bloque.
Dado throw que una instrucción transfiere el control incondicionalmente a otra parte, el punto final de una
throw instrucción nunca es accesible.
Cuando se produce una excepción, el control se transfiere a la primera catch cláusula de una instrucción de
inclusión try que puede controlar la excepción. El proceso que tiene lugar desde el punto de la excepción que
se está iniciando hasta el punto de transferir el control a un controlador de excepciones adecuado se conoce
como *propagación de excepciones _. La propagación de una excepción consiste en evaluar repetidamente
los siguientes pasos hasta que catch se encuentre una cláusula que coincida con la excepción. En esta
descripción, el punto _ Throw * es inicialmente la ubicación en la que se produce la excepción.
En el miembro de función actual, try se examina cada instrucción que incluye el punto de inicio. Para
cada instrucción S , empezando por la instrucción más interna try y finalizando con la try instrucción
externa, se evalúan los pasos siguientes:
Si el bloque de incluye S el punto de inicio y si S tiene una o más catch cláusulas, las
try
catch cláusulas se examinan en orden de aparición para encontrar un controlador adecuado para
la excepción, de acuerdo con las reglas especificadas en la sección instrucción try. Si catch se
encuentra una cláusula coincidente, la propagación de excepciones se completa mediante la
transferencia del control al bloque de esa catch cláusula.
De lo contrario, si el try bloque o un catch bloque de incluye S el punto de inicio y si S tiene
un finally bloque, el control se transfiere al finally bloque. Si el finally bloque produce otra
excepción, finaliza el procesamiento de la excepción actual. De lo contrario, cuando el control
alcanza el punto final del finally bloque, continúa el procesamiento de la excepción actual.
Si no se encuentra un controlador de excepciones en la invocación de función actual, se termina la
invocación de la función y se produce uno de los siguientes casos:
Si la función actual no es asincrónica, los pasos anteriores se repiten para el llamador de la función
con un punto de inicio correspondiente a la instrucción de la que se invocó el miembro de función.
Si la función actual es asincrónica y devuelve la tarea, la excepción se registra en la tarea devuelta,
que se coloca en un estado de error o cancelado, tal y como se describe en interfaces de
enumerador.
Si la función actual es asincrónica y devuelve void, el contexto de sincronización del subproceso
actual se notifica como se describe en interfaces enumerables.
Si el procesamiento de excepciones finaliza todas las invocaciones de miembros de función en el
subproceso actual, lo que indica que el subproceso no tiene ningún controlador para la excepción, el
subproceso se termina. El impacto de dicha terminación está definido por la implementación.
Instrucción try
La try instrucción proporciona un mecanismo para detectar las excepciones que se producen durante la
ejecución de un bloque. Además, la try instrucción proporciona la capacidad de especificar un bloque de
código que siempre se ejecuta cuando el control sale de la try instrucción.
try_statement
: 'try' block catch_clause+
| 'try' block finally_clause
| 'try' block catch_clause+ finally_clause
;
catch_clause
: 'catch' exception_specifier? exception_filter? block
;
exception_specifier
: '(' type identifier? ')'
;
exception_filter
: 'when' '(' expression ')'
;
finally_clause
: 'finally' block
;
Cuando una catch cláusula especifica un exception_specifier, el tipo debe ser System.Exception , un tipo que se
deriva de System.Exception o un tipo de parámetro de tipo que tiene System.Exception (o una subclase de ella)
como su clase base efectiva.
Cuando una catch cláusula especifica un exception_specifier con un identificador, se declara una variable de
excepción * _ del nombre y tipo especificados. La variable de excepción corresponde a una variable local con un
ámbito que se extiende por encima de la catch cláusula. Durante la ejecución del _exception_filter * y el bloque,
la variable de excepción representa la excepción que se está controlando actualmente. A efectos de la
comprobación de asignación definitiva, la variable de excepción se considera asignada definitivamente en todo
el ámbito.
A menos que una catch cláusula incluya un nombre de variable de excepción, no es posible tener acceso al
objeto de excepción en el filtro y catch bloque.
Una catch cláusula que no especifica un exception_specifier se denomina catch cláusula general.
Algunos lenguajes de programación pueden admitir excepciones que no pueden representarse como un objeto
derivado de System.Exception , aunque estas excepciones nunca podrían generarse en el código de C#. catch
Se puede usar una cláusula general para detectar tales excepciones. Por lo tanto, una catch cláusula general es
semánticamente diferente de una que especifica el tipo System.Exception , en que la primera también puede
detectar excepciones de otros lenguajes.
Para buscar un controlador de una excepción, las catch cláusulas se examinan en orden léxico. Si una catch
cláusula especifica un tipo pero no un filtro de excepción, se trata de un error en tiempo de compilación para
una catch cláusula posterior en la misma try instrucción para especificar un tipo que sea igual o derivado de
ese tipo. Si una catch cláusula no especifica ningún tipo y no hay ningún filtro, debe ser la última catch
cláusula para esa try instrucción.
Dentro de un catch bloque, throw se puede usar una instrucción (instrucción throw) sin expresión para volver
a producir la excepción detectada por el catch bloque. Las asignaciones a una variable de excepción no
modifican la excepción que se vuelve a iniciar.
En el ejemplo
using System;
class Test
{
static void F() {
try {
G();
}
catch (Exception e) {
Console.WriteLine("Exception in F: " + e.Message);
e = new Exception("F");
throw; // re-throw
}
}
el método F detecta una excepción, escribe información de diagnóstico en la consola, modifica la variable de
excepción y vuelve a producir la excepción. La excepción que se vuelve a producir es la excepción original, por lo
que la salida generada es:
Exception in F: G
Exception in Main: G
Si se produjo el primer bloque catch e en lugar de volver a producir la excepción actual, la salida generada
sería la siguiente:
Exception in F: G
Exception in Main: F
Es un error en tiempo de compilación para una break continue instrucción, o goto para transferir el control
fuera de un finally bloque. Cuando una break continue instrucción, o goto aparece en un finally bloque,
el destino de la instrucción debe estar dentro del mismo finally bloque o, de lo contrario, se produce un error
en tiempo de compilación.
Se trata de un error en tiempo de compilación para return que una instrucción se produzca en un finally
bloque.
Una try instrucción se ejecuta de la siguiente manera:
El control se transfiere al try bloque.
When y si el control alcanza el punto final del try bloque:
Si la try instrucción tiene un finally bloque, finally se ejecuta el bloque.
El control se transfiere al punto final de la try instrucción.
Si una excepción se propaga a la try instrucción durante la ejecución del try bloque:
Las catch cláusulas, si las hay, se examinan en orden de aparición para encontrar un controlador
adecuado para la excepción. Si una catch cláusula no especifica un tipo, o especifica el tipo de
excepción o un tipo base del tipo de excepción:
Si la catch cláusula declara una variable de excepción, el objeto de excepción se asigna a la
variable de excepción.
Si la catch cláusula declara un filtro de excepción, se evalúa el filtro. Si se evalúa como false ,
la cláusula catch no es una coincidencia y la búsqueda continúa a través catch de las cláusulas
posteriores de un controlador adecuado.
De lo contrario, la catch cláusula se considera una coincidencia y el control se transfiere al
catch bloque coincidente.
When y si el control alcanza el punto final del catch bloque:
Si la try instrucción tiene un finally bloque, finally se ejecuta el bloque.
El control se transfiere al punto final de la try instrucción.
Si una excepción se propaga a la try instrucción durante la ejecución del catch bloque:
Si la try instrucción tiene un finally bloque, finally se ejecuta el bloque.
La excepción se propaga a la siguiente instrucción de inclusión try .
Si la try instrucción no tiene catch cláusulas o si ninguna catch cláusula coincide con la excepción:
Si la try instrucción tiene un finally bloque, finally se ejecuta el bloque.
La excepción se propaga a la siguiente instrucción de inclusión try .
Las instrucciones de un finally bloque siempre se ejecutan cuando el control sale de una try instrucción.
Esto es cierto si la transferencia de control se produce como resultado de la ejecución normal, como resultado
de la ejecución de una break continue instrucción,, goto o return , o como resultado de propagar una
excepción fuera de la try instrucción.
Si se produce una excepción durante la ejecución de un finally bloque y no se detecta en el mismo bloque
Finally, la excepción se propaga a la siguiente instrucción envolvente try . Si hay otra excepción en el proceso
de propagación, se perderá esa excepción. El proceso de propagación de una excepción se describe con más
detalle en la descripción de la throw instrucción (la instrucción throw).
El try bloque de una try instrucción es accesible si la try instrucción es accesible.
catch Se puede tener acceso a un bloque de una try instrucción si la try instrucción es accesible.
El finally bloque de una try instrucción es accesible si la try instrucción es accesible.
El punto final de una try instrucción es accesible si se cumplen las dos condiciones siguientes:
El punto final del try bloque es accesible o catch se puede tener acceso al punto final de al menos un
bloque.
Si finally hay un bloque, finally se puede tener acceso al punto final del bloque.
unchecked_statement
: 'unchecked' block
;
La checked instrucción hace que todas las expresiones del bloque se evalúen en un contexto comprobado, y la
unchecked instrucción hace que todas las expresiones del bloque se evalúen en un contexto no comprobado.
Las checked unchecked instrucciones y son exactamente equivalentes a checked los unchecked operadores y
(los operadores Checked y unchecked), salvo que operan en bloques en lugar de en expresiones.
lock (instrucción)
La lock instrucción obtiene el bloqueo de exclusión mutua para un objeto determinado, ejecuta una instrucción
y, a continuación, libera el bloqueo.
lock_statement
: 'lock' '(' expression ')' embedded_statement
;
La expresión de una lock instrucción debe indicar un valor de un tipo conocido como reference_type. No se
realiza ninguna conversión boxing implícita (conversiones boxing) en la expresión de una lock instrucción y,
por lo tanto, es un error en tiempo de compilación para que la expresión denote un valor de un value_type.
Una lock instrucción con el formato
La instrucción using
La using instrucción obtiene uno o más recursos, ejecuta una instrucción y, a continuación, desecha el recurso.
using_statement
: 'using' '(' resource_acquisition ')' embedded_statement
;
resource_acquisition
: local_variable_declaration
| expression
;
Un recurso es una clase o estructura que implementa System.IDisposable , que incluye un único método sin
parámetros denominado Dispose . El código que usa un recurso puede llamar Dispose a para indicar que el
recurso ya no es necesario. Si Dispose no se llama a, la eliminación automática se produce finalmente como
consecuencia de la recolección de elementos no utilizados.
Si el formato de resource_acquisition es local_variable_declaration , el tipo de la local_variable_declaration debe
ser dynamic o un tipo que se pueda convertir implícitamente en System.IDisposable . Si el formato de
resource_acquisition es Expression , esta expresión debe poder convertirse implícitamente en
System.IDisposable .
Las variables locales declaradas en un resource_acquisition son de solo lectura y deben incluir un inicializador.
Se produce un error en tiempo de compilación si la instrucción incrustada intenta modificar estas variables
locales (a través de la asignación o los ++ -- operadores y), tomar la dirección de ellas o pasarlas como ref
out parámetros o.
Una using instrucción se traduce en tres partes: adquisición, uso y eliminación. El uso del recurso se adjunta
implícitamente en una try instrucción que incluye una finally cláusula. Esta finally cláusula desecha el
recurso. Si null se adquiere un recurso, no se realiza ninguna llamada a Dispose y no se produce ninguna
excepción. Si el recurso es de tipo dynamic , se convierte dinámicamente a través de una conversión dinámica
implícita (conversiones dinámicas implícitas) en IDisposable durante la adquisición para asegurarse de que la
conversión se realiza correctamente antes del uso y la eliminación.
Una using instrucción con el formato
{
ResourceType resource = expression;
try {
statement;
}
finally {
((IDisposable)resource).Dispose();
}
}
De lo contrario, cuando ResourceType es un tipo de valor que acepta valores NULL o un tipo de referencia
distinto de dynamic , la expansión es
{
ResourceType resource = expression;
try {
statement;
}
finally {
if (resource != null) ((IDisposable)resource).Dispose();
}
}
{
ResourceType resource = expression;
IDisposable d = (IDisposable)resource;
try {
statement;
}
finally {
if (d != null) d.Dispose();
}
}
tiene las mismas tres expansiones posibles. En este caso ResourceType , es implícitamente el tipo en tiempo de
compilación de expression , si tiene uno. En caso contrario, la IDisposable propia interfaz se utiliza como
ResourceType . resource No se puede obtener acceso a la variable en y no es visible para la instrucción
incrustada.
Cuando un resource_acquisition adopta la forma de un local_variable_declaration, es posible adquirir varios
recursos de un tipo determinado. Una using instrucción con el formato
using (ResourceType r1 = e1, r2 = e2, ..., rN = eN) statement
En el ejemplo siguiente se crea un archivo denominado log.txt y se escriben dos líneas de texto en el archivo.
A continuación, el ejemplo abre el mismo archivo para leer y copia las líneas de texto contenidas en la consola.
using System;
using System.IO;
class Test
{
static void Main() {
using (TextWriter w = File.CreateText("log.txt")) {
w.WriteLine("This is line one");
w.WriteLine("This is line two");
}
}
}
}
Dado que TextWriter las TextReader clases y implementan la IDisposable interfaz, el ejemplo puede utilizar
using instrucciones para asegurarse de que el archivo subyacente está cerrado correctamente después de las
operaciones de escritura o lectura.
Yield (instrucción)
La yield instrucción se usa en un bloque de iteradores (bloques) para obtener un valor para el objeto
enumerador (objetos enumerador) o el objeto enumerable (objetos enumerables) de un iterador o para señalar
el final de la iteración.
yield_statement
: 'yield' 'return' expression ';'
| 'yield' 'break' ';'
;
yield no es una palabra reservada; tiene un significado especial solo cuando se usa inmediatamente antes de
una return break palabra clave o. En otros contextos, yield se puede usar como identificador.
Hay varias restricciones sobre dónde yield puede aparecer una instrucción, tal y como se describe a
continuación.
Es un error en tiempo de compilación que una yield instrucción (de cualquier forma) aparezca fuera de un
method_body, operator_body o accessor_body
Es un error en tiempo de compilación para una yield instrucción (de cualquier forma) que aparezca dentro
de una función anónima.
Es un error en tiempo de compilación para una yield instrucción (de cualquier forma) que aparezca en la
finally cláusula de una try instrucción.
Se trata de un error en tiempo de compilación para yield return que una instrucción aparezca en cualquier
parte de una try instrucción que contenga catch cláusulas.
En el ejemplo siguiente se muestran algunos usos válidos y no válidos de las yield instrucciones.
IEnumerator<int> GetEnumerator() {
try {
yield return 1; // Ok
yield break; // Ok
}
finally {
yield return 2; // Error, yield in finally
yield break; // Error, yield in finally
}
try {
yield return 3; // Error, yield return in try...catch
yield break; // Ok
}
catch {
yield return 4; // Error, yield return in try...catch
yield break; // Ok
}
D d = delegate {
yield return 5; // Error, yield in an anonymous function
};
}
int MyMethod() {
yield return 1; // Error, wrong return type for an iterator block
}
Debe existir una conversión implícita (conversiones implícitas) del tipo de la expresión en la yield return
instrucción al tipo yield (yield Type) del iterador.
Una yield return instrucción se ejecuta de la siguiente manera:
La expresión dada en la instrucción se evalúa, se convierte implícitamente al tipo Yield y se asigna a la
Current propiedad del objeto enumerador.
La ejecución del bloque de iterador está suspendida. Si la yield return instrucción está dentro de uno o más
try bloques, los finally bloques asociados no se ejecutan en este momento.
El MoveNext método del objeto de enumerador vuelve true a su llamador, lo que indica que el objeto de
enumerador avanzó correctamente al siguiente elemento.
La siguiente llamada al método del objeto de enumerador MoveNext reanuda la ejecución del bloque de iterador
desde donde se suspendió por última vez.
Una yield break instrucción se ejecuta de la siguiente manera:
Si la yield break instrucción está delimitada por uno o más try bloques con finally bloques asociados,
el control se transfiere inicialmente al finally bloque de la try instrucción más interna. Cuando y si el
control alcanza el punto final de un finally bloque, el control se transfiere al finally bloque de la
siguiente instrucción de inclusión try . Este proceso se repite hasta que finally se hayan ejecutado los
bloques de todas las instrucciones envolventes try .
El control se devuelve al llamador del bloque de iteradores. Este es el MoveNext método o el Dispose
método del objeto de enumerador.
Dado yield break que una instrucción transfiere el control incondicionalmente a otra parte, el punto final de
una yield break instrucción nunca es accesible.
Espacios de nombres
18/09/2021 • 37 minutes to read
Los programas de C# se organizan mediante espacios de nombres. Los espacios de nombres se usan como un
sistema de organización "interno" para un programa, y como un sistema de organización "externo", una manera
de presentar los elementos de programa que se exponen a otros programas.
Las directivas Using (directivas Using) se proporcionan para facilitar el uso de espacios de nombres.
Unidades de compilación
Un compilation_unit define la estructura global de un archivo de código fuente. Una unidad de compilación
consta de cero o más using_directive s seguidos de cero o más global_attributes seguidos de cero o más
namespace_member_declaration s.
compilation_unit
: extern_alias_directive* using_directive* global_attributes? namespace_member_declaration*
;
Un programa de C# se compone de una o varias unidades de compilación, cada una de ellas en un archivo de
código fuente independiente. Cuando se compila un programa de C#, todas las unidades de compilación se
procesan juntas. Por lo tanto, las unidades de compilación pueden depender entre sí, posiblemente de manera
circular.
Los using_directive de una unidad de compilación afectan al global_attributes y
namespace_member_declaration s de esa unidad de compilación, pero no tienen ningún efecto en otras
unidades de compilación.
Los global_attributes (atributos) de una unidad de compilación permiten la especificación de atributos para el
ensamblado y el módulo de destino. Los ensamblados y los módulos actúan como contenedores físicos para los
tipos. Un ensamblado puede constar de varios módulos físicamente separados.
El namespace_member_declaration s de cada unidad de compilación de un programa contribuye a los
miembros a un solo espacio de declaración denominado espacio de nombres global. Por ejemplo:
Archivo A.cs :
class A {}
Archivo B.cs :
class B {}
Las dos unidades de compilación contribuyen al espacio de nombres global único; en este caso, se declaran dos
clases con los nombres completos A y B . Dado que las dos unidades de compilación contribuyen al mismo
espacio de declaración, habría sido un error si cada una de ellas contenía una declaración de un miembro con el
mismo nombre.
namespace_declaration
: 'namespace' qualified_identifier namespace_body ';'?
;
qualified_identifier
: identifier ('.' identifier)*
;
namespace_body
: '{' extern_alias_directive* using_directive* namespace_member_declaration* '}'
;
namespace N1.N2
{
class A {}
class B {}
}
es semánticamente equivalente a
namespace N1
{
namespace N2
{
class A {}
class B {}
}
}
Los espacios de nombres están abiertos y dos declaraciones de espacios de nombres con el mismo nombre
completo contribuyen al mismo espacio de declaración (declaraciones). En el ejemplo
namespace N1.N2
{
class A {}
}
namespace N1.N2
{
class B {}
}
las dos declaraciones de espacio de nombres anteriores contribuyen al mismo espacio de declaración; en este
caso, se declaran dos clases con los nombres completos N1.N2.A y N1.N2.B . Dado que las dos declaraciones
contribuyen al mismo espacio de declaración, se habría producido un error si cada una de ellas contenía una
declaración de un miembro con el mismo nombre.
Alias externos
Una extern_alias_directive introduce un identificador que actúa como un alias para un espacio de nombres. La
especificación del espacio de nombres con alias es externa al código fuente del programa y se aplica también a
los espacios de nombres anidados del espacio de nombres con alias.
extern_alias_directive
: 'extern' 'alias' identifier ';'
;
extern alias X;
extern alias Y;
class Test
{
X::N.A a;
X::N.B b1;
Y::N.B b2;
Y::N.C c;
}
El programa declara la existencia de los alias extern X y Y , pero las definiciones reales de los alias son
externas al programa. N.B Ahora se puede hacer referencia a las clases con el mismo nombre como X.N.B y
Y.N.B , o mediante el calificador de alias del espacio de nombres, X::N.B y Y::N.B . Se produce un error si un
programa declara un alias extern para el que no se proporciona ninguna definición externa.
Directivas Using
*El uso de directivas _ facilita el uso de espacios de nombres y tipos definidos en otros espacios de nombres.
Las directivas de uso afectan al proceso de resolución de nombres de _namespace_or_type_name * s (nombres
de espacio de nombres y tipos) y simple_name s (nombres simples), pero a diferencia de las declaraciones, las
directivas Using no aportan nuevos miembros a los espacios de declaración subyacentes de las unidades de
compilación o los espacios de nombres en los que se utilizan.
using_directive
: using_alias_directive
| using_namespace_directive
| using_static_directive
;
Un using_alias_directive (mediante directivas de alias) introduce un alias para un espacio de nombres o un tipo.
Un using_namespace_directive (mediante directivas de espacio de nombres) importa los miembros de tipo de
un espacio de nombres.
Un using_static_directive (mediante directivas estáticas) importa los tipos anidados y los miembros estáticos de
un tipo.
El ámbito de una using_directive amplía el namespace_member_declaration s de la unidad de compilación o el
cuerpo del espacio de nombres que contiene inmediatamente. El ámbito de un using_directive no incluye
específicamente sus using_directive s del mismo nivel. Por lo tanto, los using_directive del mismo nivel no
afectan entre sí y el orden en el que se escriben es insignificante.
Usar directivas de alias
Una using_alias_directive introduce un identificador que actúa como alias para un espacio de nombres o tipo
dentro de la unidad de compilación o el cuerpo del espacio de nombres que se encuentra inmediatamente.
using_alias_directive
: 'using' identifier '=' namespace_or_type_name ';'
;
Dentro de las declaraciones de miembros de una unidad de compilación o un cuerpo de espacio de nombres
que contiene un using_alias_directive, el identificador introducido por el using_alias_directive se puede usar
para hacer referencia al espacio de nombres o tipo especificado. Por ejemplo:
namespace N1.N2
{
class A {}
}
namespace N3
{
using A = N1.N2.A;
class B: A {}
}
Anteriormente, dentro de las declaraciones de miembros del N3 espacio de nombres, A es un alias para
N1.N2.A y, por lo tanto, la clase N3.B deriva de la clase N1.N2.A . Se puede obtener el mismo efecto si se crea
un alias R para N1.N2 y, a continuación, se hace referencia a R.A :
namespace N3
{
using R = N1.N2;
class B: R.A {}
}
namespace N3
{
class A {}
}
namespace N3
{
using A = N1.N2.A; // Error, A already exists
}
Anteriormente, N3 ya contiene un miembro A , por lo que es un error en tiempo de compilación para que un
using_alias_directive use ese identificador. Del mismo modo, se trata de un error en tiempo de compilación para
dos o más using_alias_directive s en la misma unidad de compilación o cuerpo del espacio de nombres para
declarar alias con el mismo nombre.
Un using_alias_directive hace que un alias esté disponible dentro de una unidad de compilación o un cuerpo de
espacio de nombres concretos, pero no aporta ningún miembro nuevo al espacio de declaración subyacente. En
otras palabras, una using_alias_directive no es transitiva sino que solo afecta a la unidad de compilación o al
cuerpo del espacio de nombres en el que se produce. En el ejemplo
namespace N3
{
using R = N1.N2;
}
namespace N3
{
class B: R.A {} // Error, R unknown
}
using R = N1.N2;
namespace N3
{
class B: R.A {}
}
namespace N3
{
class C: R.A {}
}
Al igual que los miembros normales, los nombres introducidos por using_alias_directive s están ocultos por
miembros con el mismo nombre en ámbitos anidados. En el ejemplo
using R = N1.N2;
namespace N3
{
class R {}
namespace N1.N2 {}
namespace N3
{
extern alias E;
using R1 = E.N; // OK
using R2 = N1; // OK
using R3 = N1.N2; // OK
namespace N3
{
using R1 = N1;
using R2 = N1.N2;
class B
{
N1.N2.A a; // refers to N1.N2.A
R1.N2.A b; // refers to N1.N2.A
R2.A c; // refers to N1.N2.A
}
}
los nombres N1.N2.A , R1.N2.A y R2.A son equivalentes y todos hacen referencia a la clase cuyo nombre
completo es N1.N2.A .
El uso de alias puede asignar un nombre a un tipo construido cerrado, pero no puede asignar un nombre a una
declaración de tipo genérico sin enlazar sin proporcionar argumentos de tipo. Por ejemplo:
namespace N1
{
class A<T>
{
class B {}
}
}
namespace N2
{
using W = N1.A; // Error, cannot name unbound generic type
using Z<T> = N1.A<T>; // Error, using alias cannot have type parameters
}
using_namespace_directive
: 'using' namespace_name ';'
;
Dentro de las declaraciones de miembros de una unidad de compilación o un cuerpo de espacio de nombres
que contiene un using_namespace_directive, se puede hacer referencia a los tipos contenidos en el espacio de
nombres especificado directamente. Por ejemplo:
namespace N1.N2
{
class A {}
}
namespace N3
{
using N1.N2;
class B: A {}
}
Anteriormente, dentro de las declaraciones de miembros del N3 espacio de nombres, los miembros de tipo de
N1.N2 están directamente disponibles y, por lo tanto, la clase N3.B deriva de la clase N1.N2.A .
namespace N1.N2
{
class A {}
}
namespace N3
{
using N1;
el using_namespace_directive importa los tipos incluidos en N1 , pero no los espacios de nombres anidados en
N1 . Por lo tanto, la referencia a N2.A en la declaración de B genera un error en tiempo de compilación
porque ningún miembro denominado N2 está en el ámbito.
A diferencia de un using_alias_directive, un using_namespace_directive puede importar tipos cuyos
identificadores ya estén definidos dentro de la unidad de compilación o el cuerpo del espacio de nombres
envolvente. En efecto, los nombres importados por un using_namespace_directive están ocultos por miembros
con el mismo nombre en la unidad de compilación o el cuerpo del espacio de nombres envolvente. Por ejemplo:
namespace N1.N2
{
class A {}
class B {}
}
namespace N3
{
using N1.N2;
class A {}
}
Aquí, dentro de las declaraciones de miembro del N3 espacio de nombres, A hace referencia a en N3.A lugar
de a N1.N2.A .
Cuando más de un espacio de nombres o tipo importado por using_namespace_directive s o
using_static_directive s en la misma unidad de compilación o cuerpo de espacio de nombres contienen tipos con
el mismo nombre, las referencias a ese nombre como un type_name se consideran ambiguas. En el ejemplo
namespace N1
{
class A {}
}
namespace N2
{
class A {}
}
namespace N3
{
using N1;
using N2;
N1 y N2 contienen un miembro A , y dado que las N3 importaciones, que hacen referencia a A en, N3 es un
error en tiempo de compilación. En esta situación, el conflicto se puede resolver a través de la calificación de
referencias a A o mediante la introducción de una using_alias_directive que elige un determinado A . Por
ejemplo:
namespace N3
{
using N1;
using N2;
using A = N1.A;
class C
{
public static int A;
}
namespace N2
{
using N1;
using static C;
class B
{
void M()
{
A a = new A(); // Ok, A is unambiguous as a type-name
A.Equals(2); // Error, A is ambiguous as a simple-name
}
}
}
N1 contiene un miembro de tipo A y C contiene un campo estático A , y dado N2 que importa ambos,
hacer referencia A a como simple_name es ambiguo y un error en tiempo de compilación.
Al igual que una using_alias_directive, un using_namespace_directive no aporta ningún miembro nuevo al
espacio de declaración subyacente de la unidad de compilación o espacio de nombres, sino que afecta solo a la
unidad de compilación o al cuerpo del espacio de nombres en el que aparece.
Los namespace_name a los que hace referencia un using_namespace_directive se resuelven de la misma
manera que el namespace_or_type_name al que hace referencia una using_alias_directive. Por lo tanto,
using_namespace_directive s en la misma unidad de compilación o el cuerpo del espacio de nombres no se ven
afectados entre sí y se pueden escribir en cualquier orden.
Usar directivas estáticas
Un using_static_directive importa los tipos anidados y los miembros estáticos contenidos directamente en una
declaración de tipos en la unidad de compilación o el cuerpo del espacio de nombres que se encuentra
inmediatamente, lo que permite usar el identificador de cada miembro y tipo sin calificación.
using_static_directive
: 'using' 'static' type_name ';'
;
Dentro de las declaraciones de miembros de una unidad de compilación o un cuerpo de espacio de nombres
que contiene un using_static_directive, se puede hacer referencia a los tipos anidados y miembros estáticos
accesibles (excepto los métodos de extensión) contenidos directamente en la declaración del tipo dado. Por
ejemplo:
namespace N1
{
class A
{
public class B{}
public static B M(){ return new B(); }
}
}
namespace N2
{
using static N1.A;
class C
{
void N() { B b = M(); }
}
}
Anteriormente, dentro de las declaraciones de miembros del N2 espacio de nombres, los miembros estáticos y
los tipos anidados de N1.A están disponibles directamente y, por tanto, el método N puede hacer referencia a
los B M miembros y de N1.A .
Un using_static_directive específicamente no importa los métodos de extensión directamente como métodos
estáticos, pero los pone a disposición de la invocación de métodos de extensión (invocacionesde métodos de
extensión). En el ejemplo
namespace N1
{
static class A
{
public static void M(this string s){}
}
}
namespace N2
{
using static N1.A;
class B
{
void N()
{
M("A"); // Error, M unknown
"B".M(); // Ok, M known as extension method
N1.A.M("C"); // Ok, fully qualified
}
}
}
el using_static_directive importa el método M de extensión contenido en N1.A , pero solo como un método de
extensión. Por lo tanto, la primera referencia a M en el cuerpo de B.N produce un error en tiempo de
compilación porque ningún miembro denominado M está en el ámbito.
Un using_static_directive solo importa miembros y tipos declarados directamente en el tipo dado, no miembros
y tipos declarados en clases base.
TODO: ejemplo
En el uso de las directivas de espacio de nombresse describen las ambigüedades entre varios
using_namespace_directives y using_static_directives .
Miembros del espacio de nombres
Un namespace_member_declaration es un namespace_declaration (declaraciones de espacio de nombres) o un
type_declaration (declaraciones de tipos).
namespace_member_declaration
: namespace_declaration
| type_declaration
;
Declaraciones de tipos
Una type_declaration es una class_declaration (declaraciones de clase), una struct_declaration (declaraciones de
struct), una interface_declaration (declaraciones de interfaz), una enum_declaration (declaraciones de
enumeración) o una delegate_declaration (declaraciones de delegado).
type_declaration
: class_declaration
| struct_declaration
| interface_declaration
| enum_declaration
| delegate_declaration
;
Un type_declaration puede producirse como una declaración de nivel superior en una unidad de compilación o
como una declaración de miembro dentro de un espacio de nombres, una clase o un struct.
Cuando una declaración de tipos para un tipo T se produce como una declaración de nivel superior en una
unidad de compilación, el nombre completo del tipo recién declarado es simplemente T . Cuando se produce
una declaración de tipos para un tipo T dentro de un espacio de nombres, clase o struct, el nombre completo
del tipo recién declarado es N.T , donde N es el nombre completo del espacio de nombres, la clase o el struct
que lo contiene.
Un tipo declarado dentro de una clase o struct se denomina tipo anidado (tipos anidados).
Los modificadores de acceso permitidos y el acceso predeterminado de una declaración de tipo dependen del
contexto en el que tiene lugar la declaración (declarada accesibilidad):
Los tipos declarados en unidades de compilación o espacios de nombres pueden tener public internal
acceso o. El valor predeterminado es internal Access.
Los tipos declarados en las clases pueden tener public protected internal acceso,,, protected internal o
private . El valor predeterminado es private Access.
Los tipos declarados en Structs pueden tener public internal acceso, o private . El valor predeterminado
es private Access.
qualified_alias_member
: identifier '::' identifier type_argument_list?
;
namespace N
{
public class A {}
public class B {}
}
namespace N
{
using A = System.IO;
class X
{
A.Stream s1; // Error, A is ambiguous
A::Stream s2; // Ok
}
}
el nombre A tiene dos significados posibles en el segundo cuerpo del espacio de nombres porque tanto la
clase A como el alias using A están en el ámbito. Por esta razón, el uso de A en el nombre completo
A.Stream es ambiguo y provoca que se produzca un error en tiempo de compilación. Sin embargo, el uso de A
con el :: calificador no es un error porque A solo se busca como un alias de espacio de nombres.
Clases
18/09/2021 • 327 minutes to read
Una clase es una estructura de datos que puede contener miembros de datos (constantes y campos), miembros
de función (métodos, propiedades, eventos, indizadores, operadores, constructores de instancias, destructores y
constructores estáticos) y tipos anidados. Los tipos de clase admiten la herencia, un mecanismo mediante el cual
una clase derivada puede extender y especializar una clase base.
Declaraciones de clase
Una class_declaration es una type_declaration (declaraciones de tipos) que declara una nueva clase.
class_declaration
: attributes? class_modifier* 'partial'? 'class' identifier type_parameter_list?
class_base? type_parameter_constraints_clause* class_body ';'?
;
class_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'abstract'
| 'sealed'
| 'static'
| class_modifier_unsafe
;
Es un error en tiempo de compilación que el mismo modificador aparezca varias veces en una declaración de
clase.
El new modificador se permite en las clases anidadas. Especifica que la clase oculta un miembro heredado con
el mismo nombre, tal y como se describe en el modificador New. Se trata de un error en tiempo de compilación
para new que el modificador aparezca en una declaración de clase que no es una declaración de clase anidada.
Los public protected internal modificadores,, y private controlan la accesibilidad de la clase. Dependiendo
del contexto en el que se produce la declaración de clase, es posible que algunos de estos modificadores no
estén permitidos (sedeclare accesibilidad).
Los abstract sealed static modificadores, y se describen en las secciones siguientes.
Clases abstractas
El abstract modificador se usa para indicar que una clase está incompleta y que está destinada a usarse solo
como una clase base. Una clase abstracta es distinta de una clase no abstracta de las siguientes maneras:
No se puede crear una instancia de una clase abstracta directamente y es un error en tiempo de compilación
utilizar el new operador en una clase abstracta. Aunque es posible tener variables y valores cuyos tipos en
tiempo de compilación sean abstractos, tales variables y valores serán necesariamente null o contendrán
referencias a instancias de clases no abstractas derivadas de los tipos abstractos.
Se permite que una clase abstracta contenga miembros abstractos (aunque no es necesario).
Una clase abstracta no puede ser sellada.
Cuando una clase no abstracta se deriva de una clase abstracta, la clase no abstracta debe incluir las
implementaciones reales de todos los miembros abstractos heredados, con lo que se reemplazan los miembros
abstractos. En el ejemplo
abstract class A
{
public abstract void F();
}
abstract class B: A
{
public void G() {}
}
class C: B
{
public override void F() {
// actual implementation of F
}
}
la clase abstracta A presenta un método abstracto F . La clase B introduce un método adicional G , pero
como no proporciona una implementación de F , B también debe declararse como Abstract. La clase C
invalida F y proporciona una implementación real. Dado que no hay miembros abstractos en C , C se
permite (pero no es necesario) que no sea abstracto.
Clases selladas
El sealed modificador se usa para evitar la derivación de una clase. Se produce un error en tiempo de
compilación si se especifica una clase sellada como la clase base de otra clase.
Una clase sellada no puede ser también una clase abstracta.
El sealed modificador se usa principalmente para evitar la derivación no deseada, pero también permite ciertas
optimizaciones en tiempo de ejecución. En concreto, dado que se sabe que una clase sellada nunca tiene clases
derivadas, es posible transformar las invocaciones de miembros de función virtual en instancias de clase
selladas en invocaciones no virtuales.
Clases estáticas
El static modificador se usa para marcar la clase que se declara como una clase estática . No se puede crear
una instancia de una clase estática, no se puede usar como un tipo y solo puede contener miembros estáticos.
Solo una clase estática puede contener declaraciones de métodos de extensión (métodos de extensión).
Una declaración de clase estática está sujeta a las siguientes restricciones:
Una clase estática no puede incluir un sealed abstract modificador o. Sin embargo, tenga en cuenta que,
puesto que no se pueden crear instancias de una clase estática ni derivarse de, se comporta como si fuera
Sealed y Abstract.
Una clase estática no puede incluir una especificación de class_base (clase base Specification) y no puede
especificar explícitamente una clase base o una lista de interfaces implementadas. Una clase estática hereda
implícitamente del tipo object .
Una clase estática solo puede contener miembros estáticos (miembros estáticos y de instancia). Tenga en
cuenta que las constantes y los tipos anidados se clasifican como miembros estáticos.
Una clase estática no puede tener miembros con protected o protected internal declarar accesibilidad.
Se permite que un primary_expression (miembros de función) haga referencia a una clase estática Si
La primary_expression es E en un member_access (comprobación en tiempo de compilación de la
resolución dinámica de sobrecarga) del formulario E.I .
En cualquier otro contexto, se trata de un error en tiempo de compilación para hacer referencia a una clase
estática. Por ejemplo, es un error que una clase estática se use como una clase base, un tipo constituyente (tipos
anidados) de un miembro, un argumento de tipo genérico o una restricción de parámetro de tipo. Del mismo
modo, una clase estática no se puede usar en un tipo de matriz, un tipo de puntero, una expresión, una
expresión de conversión, una expresión, una expresión, new is una expresión as sizeof o una expresión de
valor predeterminado.
Unmodifier (modificador)
El partial modificador se usa para indicar que este class_declaration es una declaración de tipos parciales.
Varias declaraciones de tipos parciales con el mismo nombre dentro de una declaración de tipo o espacio de
nombres envolvente se combinan para formar una declaración de tipos, siguiendo las reglas especificadas en
tipos parciales.
Tener la declaración de una clase distribuida sobre segmentos independientes del texto del programa puede ser
útil si estos segmentos se producen o mantienen en contextos diferentes. Por ejemplo, una parte de una
declaración de clase puede ser generada por el equipo, mientras que la otra se crea manualmente. La separación
textual de los dos impide que las actualizaciones se realicen en conflicto con las actualizaciones del otro.
Parámetros de tipo
Un parámetro de tipo es un identificador simple que denota un marcador de posición para un argumento de
tipo proporcionado para crear un tipo construido. Un parámetro de tipo es un marcador de posición formal para
un tipo que se proporcionará más adelante. Por el contrario, un argumento de tipo (argumentos de tipo) es el
tipo real que se sustituye por el parámetro de tipo cuando se crea un tipo construido.
type_parameter_list
: '<' type_parameters '>'
;
type_parameters
: attributes? type_parameter
| type_parameters ',' attributes? type_parameter
;
type_parameter
: identifier
;
Cada parámetro de tipo de una declaración de clase define un nombre en el espacio de declaración
(declaraciones) de esa clase. Por lo tanto, no puede tener el mismo nombre que otro parámetro de tipo o un
miembro declarado en esa clase. Un parámetro de tipo no puede tener el mismo nombre que el propio tipo.
Especificación de clase base
Una declaración de clase puede incluir una especificación de class_base , que define la clase base directa de la
clase y las interfaces (interfaces) implementadas directamente por la clase.
class_base
: ':' class_type
| ':' interface_type_list
| ':' class_type ',' interface_type_list
;
interface_type_list
: interface_type (',' interface_type)*
;
La clase base especificada en una declaración de clase puede ser un tipo de clase construido (tipos construidos).
Una clase base no puede ser un parámetro de tipo por su cuenta, aunque puede incluir los parámetros de tipo
que se encuentran en el ámbito.
Clases base
Cuando se incluye un class_type en el class_base, especifica la clase base directa de la clase que se está
declarando. Si una declaración de clase no tiene class_base, o si el class_base solo enumera los tipos de interfaz,
se supone que la clase base directa es object . Una clase hereda los miembros de su clase base directa, como
se describe en herencia.
En el ejemplo
class A {}
class B: A {}
A se dice que la clase es la clase base directa de B y B se dice que se deriva de A . Puesto que no A
especifica explícitamente una clase base directa, su clase base directa es implícitamente object .
Para un tipo de clase construido, si se especifica una clase base en la declaración de clase genérica, la clase base
del tipo construido se obtiene sustituyendo, por cada type_parameter en la declaración de clase base, el
type_argument correspondiente del tipo construido. Dadas las declaraciones de clase genéricas
class A<T> {
public class B {}
}
class C : A<C.B> {}
es un error porque, en la especificación de clase base A<C.B> , la clase base directa de C se considera object
y, por lo tanto, las reglas de nombres de espacio de nombres y de tipo C no se consideran miembros B .
Las clases base de un tipo de clase son la clase base directa y sus clases base. En otras palabras, el conjunto de
clases base es el cierre transitivo de la relación de clase base directa. En el ejemplo anterior, las clases base de B
son A y object . En el ejemplo
class A {...}
class A: B {}
class B: C {}
class C: A {}
es un error porque las clases dependen circularmente por sí mismas. Por último, el ejemplo
class A: B.C {}
class B: A
{
public class C {}
}
produce un error en tiempo de compilación porque A depende de B.C (su clase base directa), que depende de
B (su clase envolvente inmediata), que depende circularmente de A .
Tenga en cuenta que una clase no depende de las clases anidadas en ella. En el ejemplo
class A
{
class B: A {}
}
B depende de A (porque A es tanto su clase base directa como su clase envolvente inmediata), pero A no
depende de B (puesto que no B es una clase base ni una clase envolvente de A ). Por lo tanto, el ejemplo es
válido.
No se puede derivar de una sealed clase. En el ejemplo
sealed class A {}
type_parameter_constraints
: primary_constraint
| secondary_constraints
| constructor_constraint
| primary_constraint ',' secondary_constraints
| primary_constraint ',' constructor_constraint
| secondary_constraints ',' constructor_constraint
| primary_constraint ',' secondary_constraints ',' constructor_constraint
;
primary_constraint
: class_type
| 'class'
| 'struct'
;
secondary_constraints
: interface_type
| type_parameter
| secondary_constraints ',' interface_type
| secondary_constraints ',' type_parameter
;
constructor_constraint
: 'new' '(' ')'
;
Cada type_parameter_constraints_clause consta del token where , seguido del nombre de un parámetro de tipo,
seguido de dos puntos y la lista de restricciones para ese parámetro de tipo. Puede haber como máximo una
where cláusula para cada parámetro de tipo y las where cláusulas se pueden enumerar en cualquier orden. Al
igual que los get set tokens y en un descriptor de acceso de propiedad, el where token no es una palabra
clave.
La lista de restricciones dadas en una where cláusula puede incluir cualquiera de los componentes siguientes,
en este orden: una restricción Primary única, una o más restricciones secundarias y la restricción de constructor,
new() .
Una restricción Primary puede ser un tipo de clase o una restricción de tipo de referencia * class o la
restricción de tipo de valor struct . Una restricción secundaria puede ser una _type_parameter * o
interface_type.
La restricción de tipo de referencia especifica que un argumento de tipo usado para el parámetro de tipo debe
ser un tipo de referencia. Todos los tipos de clase, tipos de interfaz, tipos de delegado, tipos de matriz y
parámetros de tipo que se sabe que son un tipo de referencia (como se define a continuación) satisfacen esta
restricción.
La restricción de tipo de valor especifica que un argumento de tipo usado para el parámetro de tipo debe ser un
tipo de valor que no acepta valores NULL. Todos los tipos de struct que no aceptan valores NULL, tipos de
enumeración y parámetros de tipo con la restricción de tipo de valor cumplen esta restricción. Tenga en cuenta
que aunque se clasifique como un tipo de valor, un tipo que acepta valores NULL (tipos que aceptan valores
NULL) no satisface la restricción de tipo de valor. Un parámetro de tipo que tiene la restricción de tipo de valor
no puede tener también el constructor_constraint.
Los tipos de puntero nunca pueden ser argumentos de tipo y no se tienen en cuenta para satisfacer las
restricciones de tipo de valor o tipo de referencia.
Si una restricción es un tipo de clase, un tipo de interfaz o un parámetro de tipo, ese tipo especifica un "tipo
base" mínimo que cada argumento de tipo utilizado para ese parámetro de tipo debe admitir. Siempre que se
usa un tipo construido o un método genérico, el argumento de tipo se compara con las restricciones del
parámetro de tipo en tiempo de compilación. El argumento de tipo proporcionado debe cumplir las condiciones
descritas en satisfacción de las restricciones.
Una restricción class_type debe cumplir las siguientes reglas:
El tipo debe ser un tipo de clase.
El tipo no debe ser sealed .
El tipo no debe ser uno de los tipos siguientes: System.Array , System.Delegate , System.Enum o
System.ValueType .
El tipo no debe ser object . Dado que todos los tipos se derivan de object , este tipo de restricción no
tendría ningún efecto si se permitiera.
Como máximo, una restricción para un parámetro de tipo determinado puede ser un tipo de clase.
Un tipo especificado como restricción interface_type debe cumplir las siguientes reglas:
El tipo debe ser un tipo de interfaz.
Un tipo no debe especificarse más de una vez en una where cláusula determinada.
En cualquier caso, la restricción puede incluir cualquiera de los parámetros de tipo del tipo o la declaración de
método asociados como parte de un tipo construido, y puede implicar el tipo que se declara.
Cualquier tipo de clase o interfaz especificado como restricción de parámetro de tipo debe ser al menos igual de
accesible (restricciones de accesibilidad) que el tipo o método genérico que se está declarando.
Un tipo especificado como restricción type_parameter debe cumplir las siguientes reglas:
El tipo debe ser un parámetro de tipo.
Un tipo no debe especificarse más de una vez en una where cláusula determinada.
Además, no debe haber ningún ciclo en el gráfico de dependencias de los parámetros de tipo, donde
Dependency es una relación transitiva definida por:
Si T se usa un parámetro de tipo como una restricción para el parámetro de tipo S , S depende de T .
Si un parámetro de tipo S depende de un parámetro de tipo T y T depende de un parámetro de tipo U ,
S depende de U .
Dada esta relación, se trata de un error en tiempo de compilación para que un parámetro de tipo dependa de sí
mismo (directa o indirectamente).
Cualquier restricción debe ser coherente entre los parámetros de tipo dependiente. Si el parámetro S de tipo
depende del parámetro de tipo T , entonces:
T no debe tener la restricción de tipo de valor. De lo contrario, se sella de forma eficaz, de modo que se T
S forzaría que sea del mismo tipo que, lo que T elimina la necesidad de dos parámetros de tipo.
Si S tiene la restricción de tipo de valor, entonces T no debe tener una restricción class_type .
Si S tiene una restricción class_type A y T tiene una restricción class_type B , debe haber una conversión
de identidad o una conversión de referencia implícita de A a B o una conversión de referencia implícita de
a B A .
Si S también depende del parámetro de tipo U y U tiene una restricción class_type A y T tiene una
restricción class_type , debe haber una conversión de B identidad o una conversión de referencia implícita
de A a B o una conversión de referencia implícita de B a A .
Es válido para S que tenga la restricción de tipo de valor y T para que tenga la restricción de tipo de
referencia. De hecho, esto limita T a los tipos System.Object ,, y a System.ValueType System.Enum cualquier
tipo de interfaz.
Si la where cláusula de un parámetro de tipo incluye una restricción de constructor (que tiene el formato new()
), es posible utilizar el new operador para crear instancias del tipo (expresiones de creación de objetos).
Cualquier argumento de tipo utilizado para un parámetro de tipo con una restricción de constructor debe tener
un constructor sin parámetros público (este constructor existe implícitamente para cualquier tipo de valor) o ser
un parámetro de tipo con la restricción de tipo de valor o la restricción de constructor (vea restricciones de
parámetro de tipo para obtener más detalles).
A continuación se muestran ejemplos de restricciones:
interface IPrintable
{
void Print();
}
interface IComparable<T>
{
int CompareTo(T value);
}
interface IKeyProvider<T>
{
T GetKey();
}
class Dictionary<K,V>
where K: IComparable<K>
where V: IPrintable, IKeyProvider<K>, new()
{
...
}
El ejemplo siguiente es erróneo porque provoca una circularidad en el gráfico de dependencias de los
parámetros de tipo:
class Circular<S,T>
where S: T
where T: S // Error, circularity in dependency graph
{
...
}
class A {...}
class B {...}
class Incompat<S,T>
where S: A, T
where T: B // Error, incompatible class-type constraints
{
...
}
class StructWithClass<S,T,U>
where S: struct, T
where T: U
where U: A // Error, A incompatible with struct
{
...
}
En el caso de estas reglas, si T tiene una restricción V que es un value_type, use en su lugar el tipo base más
específico de V que sea un class_type. Esto nunca puede ocurrir en una restricción explícitamente especificada,
pero puede producirse cuando las restricciones de un método genérico se heredan implícitamente mediante
una declaración de método de reemplazo o una implementación explícita de un método de interfaz.
Estas reglas garantizan que la clase base efectiva siempre sea una class_type.
El conjunto de interfaces efectivo de un parámetro de tipo T se define de la siguiente manera:
Si T no tiene secondary_constraints, su conjunto de interfaces efectivo está vacío.
Si T tiene restricciones de interface_type pero no type_parameter restricciones, su conjunto de interfaces
efectivo es su conjunto de restricciones de interface_type .
Si T no tiene restricciones de interface_type pero tiene type_parameter restricciones, su conjunto de
interfaces efectivo es la Unión de los conjuntos de interfaces eficaces de sus restricciones de type_parameter .
Si T tiene tanto restricciones interface_type como restricciones de type_parameter , su conjunto de
interfaces efectivo es la Unión de su conjunto de restricciones interface_type y los conjuntos de interfaces
eficaces de sus restricciones de type_parameter .
Se sabe que un parámetro de tipo es un tipo de referencia si tiene la restricción de tipo de referencia o su
clase base efectiva no es object o System.ValueType .
Los valores de un tipo de parámetro de tipo restringido se pueden utilizar para tener acceso a los miembros de
instancia que implican las restricciones. En el ejemplo
interface IPrintable
{
void Print();
}
los métodos de IPrintable se pueden invocar directamente en x porque T está restringido a implementar
siempre IPrintable .
Cuerpo de clase
El class_body de una clase define los miembros de esa clase.
class_body
: '{' class_member_declaration* '}'
;
Tipos parciales
Una declaración de tipos se puede dividir en varias declaraciones de tipos parciales . La declaración de tipos
se construye a partir de sus elementos siguiendo las reglas de esta sección, en la que se trata como una única
declaración durante el resto del procesamiento en tiempo de compilación y en tiempo de ejecución del
programa.
Un class_declaration, struct_declaration o interface_declaration representa una declaración de tipos parciales si
incluye un partial modificador. partial no es una palabra clave y solo actúa como modificador si aparece
inmediatamente antes de una de las palabras clave class , struct o interface en una declaración de tipo o
antes del tipo void en una declaración de método. En otros contextos, se puede usar como un identificador
normal.
Cada parte de una declaración de tipo parcial debe incluir un partial modificador. Debe tener el mismo
nombre y estar declarado en el mismo espacio de nombres o declaración de tipos que las demás partes. El
partial modificador indica que pueden existir partes adicionales de la declaración de tipos en otro lugar, pero
la existencia de tales partes adicionales no es un requisito; es válido para un tipo con una sola declaración que
incluya el partial modificador.
Todas las partes de un tipo parcial se deben compilar de forma que se puedan combinar las partes en tiempo de
compilación en una única declaración de tipo. Los tipos parciales no permiten que se extiendan los tipos ya
compilados.
Los tipos anidados se pueden declarar en varias partes mediante el partial modificador. Normalmente, el tipo
contenedor se declara utilizando partial también y cada parte del tipo anidado se declara en una parte
diferente del tipo contenedor.
partial No se permite el modificador en las declaraciones de delegado o enumeración.
Atributos
Los atributos de un tipo parcial se determinan mediante la combinación, en un orden no especificado, con los
atributos de cada uno de los elementos. Si un atributo se coloca en varias partes, es equivalente a especificar el
atributo varias veces en el tipo. Por ejemplo, las dos partes:
[Attr1, Attr2("hello")]
partial class A {}
[Attr3, Attr2("goodbye")]
partial class A {}
es correcto porque las partes que incluyen restricciones (las dos primeras) especifican de forma eficaz el mismo
conjunto de restricciones principales, secundarias y constructores para el mismo conjunto de parámetros de
tipo, respectivamente.
Clase base
Cuando una declaración de clase parcial incluye una especificación de clase base, debe coincidir con todas las
demás partes que incluyen una especificación de clase base. Si ninguna parte de una clase parcial incluye una
especificación de clase base, la clase base se convierte en System.Object (clases base).
Interfaces base
El conjunto de interfaces base para un tipo declarado en varias partes es la Unión de las interfaces base
especificadas en cada parte. Solo se puede asignar un nombre a una interfaz base determinada una vez en cada
parte, pero se permite que varias partes denominen a las mismas interfaces base. Solo debe haber una
implementación de los miembros de una interfaz base determinada.
En el ejemplo
partial class X
{
int IComparable.CompareTo(object o) {...}
}
partial class A
{
int x; // Error, cannot declare x more than once
partial class A
{
int x; // Error, cannot declare x more than once
El orden de los miembros dentro de un tipo rara vez es significativo para el código de C#, pero puede ser
importante al interactuar con otros lenguajes y entornos. En estos casos, el orden de los miembros dentro de un
tipo declarado en varias partes es indefinido.
Métodos parciales
Los métodos parciales se pueden definir en una parte de una declaración de tipos e implementarse en otro. La
implementación es opcional. Si ninguna parte implementa el método parcial, la declaración de método parcial y
todas las llamadas a ella se quitan de la declaración de tipos resultante de la combinación de los elementos.
Los métodos parciales no pueden definir modificadores de acceso, pero son implícitamente private . Su tipo
de valor devuelto debe ser void , y sus parámetros no pueden tener el out modificador. El identificador
partial se reconoce como una palabra clave especial en una declaración de método solo si aparece justo antes
del void tipo; en caso contrario, se puede usar como un identificador normal. Un método parcial no puede
implementar explícitamente métodos de interfaz.
Hay dos tipos de declaraciones de método parcial: Si el cuerpo de la declaración del método es un punto y
coma, se dice que la declaración es una * que define una declaración de método parcial _. Si el cuerpo se
proporciona como _block *, se dice que la declaración es una declaración de método parcial de
implementación . En las partes de una declaración de tipos solo puede haber una declaración de método
parcial de definición con una firma determinada, y solo puede haber una declaración de método parcial de
implementación con una firma determinada. Si se proporciona una declaración de método parcial de
implementación, debe existir una declaración de método parcial de definición correspondiente, y las
declaraciones deben coincidir como se especifica en lo siguiente:
Las declaraciones deben tener los mismos modificadores (aunque no necesariamente en el mismo orden), el
nombre del método, el número de parámetros de tipo y el número de parámetros.
Los parámetros correspondientes en las declaraciones deben tener los mismos modificadores (aunque no
necesariamente en el mismo orden) y los mismos tipos (diferencias de módulo en los nombres de
parámetros de tipo).
Los parámetros de tipo correspondientes en las declaraciones deben tener las mismas restricciones
(diferencias de módulo en los nombres de parámetros de tipo).
Una declaración de método parcial de implementación puede aparecer en la misma parte que la declaración de
método parcial de definición correspondiente.
Solo un método parcial de definición participa en la resolución de sobrecarga. Por lo tanto, tanto si se
proporciona una declaración de implementación como si no, las expresiones de invocación pueden resolverse
en invocaciones del método parcial. Dado que un método parcial siempre devuelve void , estas expresiones de
invocación siempre serán instrucciones de expresión. Además, dado que un método parcial es implícitamente
private , estas instrucciones siempre se producirán dentro de una de las partes de la declaración de tipos en la
que se declara el método parcial.
Si ninguna parte de una declaración de tipo parcial contiene una declaración de implementación para un
método parcial determinado, cualquier instrucción de expresión que lo invoque simplemente se quita de la
declaración de tipos combinados. Por lo tanto, la expresión de invocación, incluidas las expresiones
constituyentes, no tiene ningún efecto en tiempo de ejecución. También se quita el método parcial en sí y no
será miembro de la declaración de tipos combinados.
Si existe una declaración de implementación para un método parcial determinado, se conservan las
invocaciones de los métodos parciales. El método parcial da lugar a una declaración de método similar a la
declaración de método parcial de implementación, excepto para lo siguiente:
El partial modificador no está incluido
Los atributos de la declaración del método resultante son los atributos combinados de la definición de y la
declaración de método parcial de implementación en un orden no especificado. No se quitan los duplicados.
Los atributos de los parámetros de la declaración de método resultante son los atributos combinados de los
parámetros correspondientes de la definición de y la declaración de método parcial de implementación en
un orden no especificado. No se quitan los duplicados.
Si se proporciona una declaración de definición pero no una declaración de implementación para un método
parcial M, se aplican las restricciones siguientes:
Es un error en tiempo de compilación crear un delegado para el método (expresiones de creación de
delegado).
Es un error en tiempo de compilación hacer referencia a M dentro de una función anónima que se convierte
en un tipo de árbolde expresión (evaluación de conversiones de funciones anónimas a tipos de árbol de
expresión).
Las expresiones que se producen como parte de una invocación de no M afectan al estado de asignación
definitiva (asignación definitiva), lo que puede provocar errores en tiempo de compilación.
M no puede ser el punto de entrada de una aplicación (inicio de laaplicación).
Los métodos parciales son útiles para permitir que una parte de una declaración de tipos Personalice el
comportamiento de otra parte, por ejemplo, una generada por una herramienta. Considere la siguiente
declaración de clase parcial:
partial class Customer
{
string name;
Si esta clase se compila sin ningún otro elemento, se quitarán las declaraciones de método parcial de definición
y sus invocaciones, y la declaración de clase combinada resultante será equivalente a la siguiente:
class Customer
{
string name;
No obstante, supongamos que se proporciona otra parte, que proporciona declaraciones de implementación de
los métodos parciales:
void OnNameChanged()
{
Console.WriteLine("Changed to " + name);
}
}
Enlace de nombre
Aunque cada parte de un tipo extensible se debe declarar dentro del mismo espacio de nombres, las partes se
escriben normalmente en diferentes declaraciones de espacio de nombres. Por lo tanto, using pueden estar
presentes directivas diferentes (mediante directivas) para cada parte. Al interpretar nombres simples (inferencia
de tipos) dentro de una parte, solo using se tienen en cuenta las directivas de las declaraciones de espacio de
nombres que la forman. Esto puede dar lugar a que el mismo identificador tenga significados diferentes en
distintas partes:
namespace N
{
using List = System.Collections.ArrayList;
partial class A
{
List x; // x has type System.Collections.ArrayList
}
}
namespace N
{
using List = Widgets.LinkedList;
partial class A
{
List y; // y has type Widgets.LinkedList
}
}
Miembros de clase
Los miembros de una clase se componen de los miembros introducidos por su class_member_declaration s y
los miembros heredados de la clase base directa.
class_member_declaration
: constant_declaration
| field_declaration
| method_declaration
| property_declaration
| event_declaration
| indexer_declaration
| operator_declaration
| constructor_declaration
| destructor_declaration
| static_constructor_declaration
| type_declaration
;
class Gen<T,U>
{
public T[,] a;
public void G(int i, T t, Gen<U,T> gt) {...}
public U Prop { get {...} set {...} }
public int H(double d) {...}
}
public int[,][] a;
public void G(int i, int[] t, Gen<IComparable<string>,int[]> gt) {...}
public IComparable<string> Prop { get {...} set {...} }
public int H(double d) {...}
El tipo del miembro a en la declaración de clase genérica Gen es la "matriz bidimensional de T ", por lo que
el tipo del miembro a en el tipo construido anterior es la "matriz bidimensional de una matriz unidimensional
de int ", o int[,][] .
Dentro de los miembros de la función de instancia, el tipo de this es el tipo de instancia (el tipo de instancia)
de la declaración contenedora.
Todos los miembros de una clase genérica pueden usar parámetros de tipo de cualquier clase envolvente, ya sea
directamente o como parte de un tipo construido. Cuando se usa un tipo construido cerrado determinado (tipos
abiertos y cerrados) en tiempo de ejecución, cada uso de un parámetro de tipo se reemplaza por el argumento
de tipo real proporcionado al tipo construido. Por ejemplo:
class C<V>
{
public V f1;
public C<V> f2 = null;
public C(V x) {
this.f1 = x;
this.f2 = this;
}
}
class Application
{
static void Main() {
C<int> x1 = new C<int>(1);
Console.WriteLine(x1.f1); // Prints 1
Herencia
Una clase hereda los miembros de su tipo de clase base directa. La herencia significa que una clase contiene
implícitamente todos los miembros de su tipo de clase base directa, a excepción de los constructores de
instancias, destructores y constructores estáticos de la clase base. Algunos aspectos importantes de la herencia
son:
La herencia es transitiva. Si C se deriva de B y B se deriva de A , C hereda los miembros declarados en,
así B como los miembros declarados en A .
Una clase derivada extiende su clase base directa. Una clase derivada puede agregar nuevos miembros a
aquellos de los que hereda, pero no puede quitar la definición de un miembro heredado.
Los constructores de instancias, destructores y constructores estáticos no se heredan, pero todos los demás
miembros son, independientemente de su accesibilidad declarada (acceso a miembros). Sin embargo, en
función de la accesibilidad declarada, es posible que los miembros heredados no estén accesibles en una
clase derivada.
Una clase derivada puede ocultar (ocultar a travésde la herencia) los miembros heredados declarando
nuevos miembros con el mismo nombre o signatura. Sin embargo, tenga en cuenta que ocultar un miembro
heredado no quita ese miembro; simplemente hace que ese miembro no sea accesible directamente a través
de la clase derivada.
Una instancia de una clase contiene un conjunto de todos los campos de instancia declarados en la clase y
sus clases base, y existe una conversión implícita (conversiones de referencia implícita) de un tipo de clase
derivada a cualquiera de sus tipos de clase base. Por lo tanto, una referencia a una instancia de alguna clase
derivada se puede tratar como una referencia a una instancia de cualquiera de sus clases base.
Una clase puede declarar métodos virtuales, propiedades e indizadores, y las clases derivadas pueden
invalidar la implementación de estos miembros de función. Esto permite que las clases muestren un
comportamiento polimórfico en el que las acciones realizadas por una invocación de miembro de función
varían en función del tipo en tiempo de ejecución de la instancia a través de la cual se invoca a ese miembro
de función.
El miembro heredado de un tipo de clase construido son los miembros del tipo de clase base inmediato (clases
base), que se encuentra sustituyendo los argumentos de tipo del tipo construido por cada aparición de los
parámetros de tipo correspondientes en la especificación class_base . Estos miembros, a su vez, se transforman
sustituyendo, por cada type_parameter en la declaración de miembro, el type_argument correspondiente de la
especificación de class_base .
class B<U>
{
public U F(long index) {...}
}
En el ejemplo anterior, el tipo construido D<int> tiene un miembro no heredado que se public int G(string s)
obtiene sustituyendo el argumento int de tipo para el parámetro de tipo T . D<int> también tiene un
miembro heredado de la declaración de clase B . Este miembro heredado se determina determinando en
primer lugar el tipo de clase base B<int[]> de D<int> sustituyendo int por T en la especificación de clase
base B<T[]> . A continuación, como un argumento de tipo en B , int[] se sustituye por U en, lo que
public U F(long index) produce el miembro heredado public int[] F(long index) .
El nuevo modificador
Un class_member_declaration puede declarar un miembro con el mismo nombre o signatura que un miembro
heredado. Cuando esto ocurre, se dice que el miembro de la clase derivada oculta el miembro de la clase base.
Ocultar un miembro heredado no se considera un error, pero hace que el compilador emita una advertencia.
Para suprimir la advertencia, la declaración del miembro de la clase derivada puede incluir un new modificador
para indicar que el miembro derivado está pensado para ocultar el miembro base. Este tema se describe con
más detalle en ocultarse a travésde la herencia.
Si new se incluye un modificador en una declaración que no oculta un miembro heredado, se emite una
advertencia para ese efecto. Esta advertencia se suprime quitando el new modificador.
Modificadores de acceso
Un class_member_declaration puede tener cualquiera de los cinco tipos posibles de accesibilidad declarada
(accesibilidad declarada): public , protected internal , protected , internal o private . A excepción de la
protected internal combinación, se trata de un error en tiempo de compilación para especificar más de un
modificador de acceso. Cuando una class_member_declaration no incluye modificadores de acceso, private se
supone.
Tipos constituyentes
Los tipos que se usan en la declaración de un miembro se denominan tipos constituyentes de ese miembro. Los
tipos constituyentes posibles son el tipo de una constante, un campo, una propiedad, un evento o un indizador,
el tipo de valor devuelto de un método o un operador, y los tipos de parámetro de un método, indizador,
operador o constructor de instancia. Los tipos constituyentes de un miembro deben ser al menos tan accesibles
como el propio miembro (restricciones de accesibilidad).
Miembros estáticos y de instancia
Los miembros de una clase son *miembros estáticos _ o _ miembros de instancia *. En general, resulta útil
pensar en que los miembros estáticos pertenecen a los tipos de clase y a los miembros de instancia como
pertenecientes a objetos (instancias de tipos de clase).
Cuando un campo, método, propiedad, evento, operador o declaración de constructor incluye un static
modificador, declara un miembro estático. Además, una declaración de constante o de tipo declara
implícitamente un miembro estático. Los miembros estáticos tienen las siguientes características:
Cuando se hace referencia a un miembro estático M en un member_access (acceso a miembros) del
formulario E.M , E debe indicar un tipo que contiene M . Se trata de un error en tiempo de compilación
para E que denote una instancia.
Un campo estático identifica exactamente una ubicación de almacenamiento que compartirán todas las
instancias de un tipo de clase cerrada determinado. Independientemente del número de instancias de un tipo
de clase cerrada determinado que se creen, solo hay una copia de un campo estático.
Un miembro de función estático (método, propiedad, evento, operador o constructor) no funciona en una
instancia específica y es un error en tiempo de compilación para hacer referencia a this en este tipo de
miembro de función.
Cuando un campo, un método, una propiedad, un evento, un indexador, un constructor o una declaración de
destructor no incluye un static modificador, declara un miembro de instancia. (Un miembro de instancia se
denomina a veces miembro no estático). Los miembros de instancia tienen las siguientes características:
Cuando M se hace referencia a un miembro de instancia en un member_access (acceso a miembros) del
formulario E.M , E debe indicar una instancia de un tipo que contenga M . Es un error en tiempo de enlace
para E que denote un tipo.
Cada instancia de una clase contiene un conjunto independiente de todos los campos de instancia de la clase.
Un miembro de función de instancia (método, propiedad, indizador, constructor de instancia o destructor)
opera en una instancia determinada de la clase y se puede tener acceso a esta instancia como this (este
acceso).
En el ejemplo siguiente se muestran las reglas para tener acceso a los miembros estáticos y de instancia:
class Test
{
int x;
static int y;
void F() {
x = 1; // Ok, same as this.x = 1
y = 1; // Ok, same as Test.y = 1
}
El F método muestra que en un miembro de función de instancia, se puede usar un simple_name (nombres
simples) para tener acceso a los miembros de instancia y a los miembros estáticos. El G método muestra que
en un miembro de función estático, es un error en tiempo de compilación para tener acceso a un miembro de
instancia a través de un simple_name. El Main método muestra que en un member_access (acceso a
miembros), se debe tener acceso a los miembros de instancia a través de instancias de y se debe tener acceso a
los miembros estáticos a través de los tipos.
Tipos anidados
Un tipo declarado dentro de una declaración de clase o struct se denomina *tipo anidado _. Un tipo que se
declara dentro de una unidad de compilación o espacio de nombres se denomina un tipo no anidado* *.
En el ejemplo
using System;
class A
{
class B
{
static void F() {
Console.WriteLine("A.B.F");
}
}
}
B la clase es un tipo anidado porque se declara dentro de la clase A y A la clase es un tipo no anidado porque
se declara dentro de una unidad de compilación.
Nombre completo
El nombre completo (nombrescompletos) de un tipo anidado es, S.N donde S es el nombre completo del tipo
en el que N se declara el tipo.
Accesibilidad declarada
Los tipos no anidados pueden tener public o internal declarar accesibilidad y internal declarar
accesibilidad de forma predeterminada. Los tipos anidados también pueden tener estas formas de accesibilidad
declarada, además de una o varias formas adicionales de accesibilidad declarada, dependiendo de si el tipo
contenedor es una clase o un struct:
Un tipo anidado que se declara en una clase puede tener cualquiera de las cinco formas de accesibilidad
declarada ( public , protected internal , protected , internal o private ) y, al igual que otros miembros
de clase, tiene como valor predeterminado la private accesibilidad declarada.
Un tipo anidado que se declara en un struct puede tener cualquiera de las tres formas de accesibilidad
declarada ( public , internal o private ) y, al igual que otros miembros de struct, tiene como valor
predeterminado la private accesibilidad declarada.
En el ejemplo
public class List
{
// Private data structure
private class Node
{
public object Data;
public Node Next;
// Public interface
public void AddToFront(object o) {...}
public void AddToBack(object o) {...}
public object RemoveFromFront() {...}
public object RemoveFromBack() {...}
public int Count { get {...} }
}
using System;
class Base
{
public static void M() {
Console.WriteLine("Base.M");
}
}
class Test
{
static void Main() {
Derived.M.F();
}
}
using System;
class C
{
int i = 123;
public Nested(C c) {
this_c = c;
}
class Test
{
static void Main() {
C c = new C();
c.F();
}
}
muestra esta técnica. Una instancia de C crea una instancia de Nested y pasa su propia this al Nested
constructor de para proporcionar el acceso posterior a C los miembros de instancia de.
Acceso a los miembros privados y protegidos del tipo contenedor.
Un tipo anidado tiene acceso a todos los miembros a los que se puede tener acceso a su tipo contenedor,
incluidos los miembros del tipo contenedor que tienen private y protected declaran la accesibilidad. En el
ejemplo
using System;
class C
{
private static void F() {
Console.WriteLine("C.F");
}
class Test
{
static void Main() {
C.Nested.G();
}
}
muestra una clase C que contiene una clase anidada Nested . Dentro de Nested , el método G llama al
método estático F definido en C y F tiene una accesibilidad declarada privada.
Un tipo anidado también puede tener acceso a los miembros protegidos definidos en un tipo base de su tipo
contenedor. En el ejemplo
using System;
class Base
{
protected void F() {
Console.WriteLine("Base.F");
}
}
class Test
{
static void Main() {
Derived.Nested n = new Derived.Nested();
n.G();
}
}
la clase anidada Derived.Nested tiene acceso al método protegido F definido en la Derived clase base, Base ,
llamando a a través de una instancia de Derived .
Tipos anidados en clases genéricas
Una declaración de clase genérica puede contener declaraciones de tipos anidados. Los parámetros de tipo de la
clase envolvente se pueden usar dentro de los tipos anidados. Una declaración de tipos anidados puede
contener parámetros de tipo adicionales que solo se aplican al tipo anidado.
Cada declaración de tipos contenida dentro de una declaración de clase genérica es implícitamente una
declaración de tipos genéricos. Al escribir una referencia a un tipo anidado dentro de un tipo genérico, el tipo
que contiene el tipo construido, incluidos sus argumentos de tipo, debe denominarse. Sin embargo, desde
dentro de la clase externa, el tipo anidado se puede usar sin calificación; el tipo de instancia de la clase externa se
puede usar implícitamente al construir el tipo anidado. En el ejemplo siguiente se muestran tres formas
correctas de hacer referencia a un tipo construido creado desde Inner ; los dos primeros son equivalentes:
class Outer<T>
{
class Inner<U>
{
public static void F(T t, U u) {...}
}
Aunque el estilo de programación es incorrecto, un parámetro de tipo de un tipo anidado puede ocultar un
miembro o un parámetro de tipo declarado en el tipo externo:
class Outer<T>
{
class Inner<T> // Valid, hides Outer's T
{
public T t; // Refers to Inner's T
}
}
T get_P();
void set_P(T value);
Ambas firmas están reservadas, aunque la propiedad sea de solo lectura o de solo escritura.
En el ejemplo
using System;
class A
{
public int P {
get { return 123; }
}
}
class B: A
{
new public int get_P() {
return 456;
}
class Test
{
static void Main() {
B b = new B();
A a = b;
Console.WriteLine(a.P);
Console.WriteLine(b.P);
Console.WriteLine(b.get_P());
}
}
una clase A define una propiedad de solo lectura P , de modo que se reservan firmas para get_P set_P los
métodos y. Una clase se B deriva de A y oculta ambas firmas reservadas. En el ejemplo se genera el resultado:
123
123
456
Ambas firmas están reservadas, aunque el indizador sea de solo lectura o de solo escritura.
Además, el nombre del miembro Item está reservado.
Nombres de miembro reservados para destructores
Para una clase que contiene un destructor(destructores), se reserva la firma siguiente:
void Finalize();
Constantes
*Constant _ es un miembro de clase que representa un valor constante: un valor que se puede calcular en
tiempo de compilación. Un _constant_declaration * introduce una o más constantes de un tipo determinado.
constant_declaration
: attributes? constant_modifier* 'const' type constant_declarators ';'
;
constant_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
;
constant_declarators
: constant_declarator (',' constant_declarator)*
;
constant_declarator
: identifier '=' constant_expression
;
Un constant_declaration puede incluir un conjunto de atributos (atributos), un new modificador (el nuevo
modificador) y una combinación válida de los cuatro modificadores de acceso (modificadores de acceso). Los
atributos y modificadores se aplican a todos los miembros declarados por el constant_declaration. Aunque las
constantes se consideran miembros estáticos, una constant_declaration no requiere ni permite un static
modificador. Es un error que el mismo modificador aparezca varias veces en una declaración de constante.
El tipo de un constant_declaration especifica el tipo de los miembros introducidos por la declaración. El tipo va
seguido de una lista de constant_declarator s, cada uno de los cuales incorpora un nuevo miembro. Un
constant_declarator consta de un identificador que denomina al miembro, seguido de un token " = ", seguido
de un constant_expression (expresiones constantes) que proporciona el valor del miembro.
El tipo especificado en una declaración de constante debe ser sbyte , byte , short , ushort , int , uint ,
long , ulong , char , float , double , decimal , bool , string , un enum_type o un reference_type. Cada
constant_expression debe producir un valor del tipo de destino o de un tipo que se pueda convertir al tipo de
destino mediante una conversión implícita (conversiones implícitas).
El tipo de una constante debe ser al menos tan accesible como la propia constante (restricciones de
accesibilidad).
El valor de una constante se obtiene en una expresión usando un simple_name (nombres simples) o un
member_access (acceso a miembros).
Una constante puede participar en una constant_expression. Por lo tanto, se puede utilizar una constante en
cualquier construcción que requiera una constant_expression. Entre los ejemplos de estas construcciones se
incluyen case etiquetas, goto case instrucciones, enum declaraciones de miembros, atributos y otras
declaraciones de constantes.
Como se describe en expresiones constantes, una constant_expression es una expresión que se puede evaluar
por completo en tiempo de compilación. Dado que la única forma de crear un valor que no sea NULL de un
reference_type distinto de string es aplicar el new operador, y dado que new no se permite el operador en un
constant_expression, el único valor posible para las constantes de reference_type s no string es null .
Cuando se desea un nombre simbólico para un valor constante, pero cuando el tipo de ese valor no se permite
en una declaración de constante, o cuando el valor no se puede calcular en tiempo de compilación mediante un
constant_expression, readonly se puede usar en su lugar un campo (campos de solo lectura).
Una declaración de constante que declara varias constantes es equivalente a varias declaraciones de constantes
únicas con los mismos atributos, modificadores y tipo. Por ejemplo
class A
{
public const double X = 1.0, Y = 2.0, Z = 3.0;
}
es equivalente a
class A
{
public const double X = 1.0;
public const double Y = 2.0;
public const double Z = 3.0;
}
Las constantes pueden depender de otras constantes en el mismo programa, siempre que las dependencias no
sean de naturaleza circular. El compilador se organiza automáticamente para evaluar las declaraciones de
constantes en el orden adecuado. En el ejemplo
class A
{
public const int X = B.Z + 1;
public const int Y = 10;
}
class B
{
public const int Z = A.Y + 1;
}
en primer lugar, el compilador evalúa A.Y , evalúa B.Z y, por último, evalúa y A.X genera los valores 10 ,
11 y 12 . Las declaraciones de constantes pueden depender de constantes de otros programas, pero dichas
dependencias solo son posibles en una dirección. Tomando como referencia el ejemplo anterior, si A y B se
declararon en programas independientes, sería posible A.X que dependa de B.Z , pero B.Z no puede
depender simultáneamente A.Y .
Campos
Un *campo _ es un miembro que representa una variable asociada con un objeto o una clase. Un
_field_declaration * introduce uno o más campos de un tipo determinado.
field_declaration
: attributes? field_modifier* type variable_declarators ';'
;
field_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'static'
| 'readonly'
| 'volatile'
| field_modifier_unsafe
;
variable_declarators
: variable_declarator (',' variable_declarator)*
;
variable_declarator
: identifier ('=' variable_initializer)?
;
variable_initializer
: expression
| array_initializer
;
Un field_declaration puede incluir un conjunto de atributos (atributos), un new modificador (el nuevo
modificador), una combinación válida de los cuatro modificadores de acceso (modificadores de acceso) y un
static modificador (campos estáticos y de instancia). Además, un field_declaration puede incluir un readonly
modificador (campos de solo lectura) o un volatile modificador (campos volátiles), pero no ambos. Los
atributos y modificadores se aplican a todos los miembros declarados por el field_declaration. Es un error que el
mismo modificador aparezca varias veces en una declaración de campo.
El tipo de un field_declaration especifica el tipo de los miembros introducidos por la declaración. El tipo va
seguido de una lista de variable_declarator s, cada uno de los cuales incorpora un nuevo miembro. Un
variable_declarator consta de un identificador que asigna un nombre a ese miembro, seguido opcionalmente
por un = token "" y un variable_initializer (inicializadores de variables) que proporciona el valor inicial de ese
miembro.
El tipo de un campo debe ser al menos igual de accesible que el propio campo (restricciones de accesibilidad).
El valor de un campo se obtiene en una expresión usando un simple_name (nombres simples) o un
member_access (acceso a miembros). El valor de un campo que no es de solo lectura se modifica mediante una
asignación (operadores de asignación). El valor de un campo que no es de solo lectura se puede obtener y
modificar mediante operadores de incremento y decremento postfijos (operadores de incremento y
decrementopostfijo) y operadores de incremento y decremento prefijos (operadores de incremento y
decremento de prefijo).
Una declaración de campo que declara varios campos es equivalente a varias declaraciones de campos únicos
con los mismos atributos, modificadores y tipo. Por ejemplo
class A
{
public static int X = 1, Y, Z = 100;
}
es equivalente a
class A
{
public static int X = 1;
public static int Y;
public static int Z = 100;
}
class C<V>
{
static int count = 0;
public C() {
count++;
}
class Application
{
static void Main() {
C<int> x1 = new C<int>();
Console.WriteLine(C<int>.Count); // Prints 1
Un campo de instancia pertenece a una instancia de. En concreto, cada instancia de una clase contiene un
conjunto independiente de todos los campos de instancia de esa clase.
Cuando se hace referencia a un campo en un member_access (acceso a miembros) del formulario E.M , si M es
un campo estático, E debe indicar un tipo que contiene M y si M es un campo de instancia, E debe denotar
una instancia de un tipo que contenga M .
Las diferencias entre los miembros estáticos y de instancia se tratan más adelante en miembros estáticos y de
instancia.
Campos de solo lectura
Cuando una field_declaration incluye un readonly modificador, los campos introducidos por la declaración son
campos de solo lectura . Las asignaciones directas a campos de solo lectura solo se pueden producir como
parte de la declaración o en un constructor de instancia o en un constructor estático de la misma clase. (Un
campo de solo lectura se puede asignar a varias veces en estos contextos). En concreto, las asignaciones directas
a un readonly campo solo se permiten en los contextos siguientes:
En el variable_declarator que presenta el campo (incluyendo un variable_initializer en la declaración).
Para un campo de instancia, en los constructores de instancia de la clase que contiene la declaración de
campo; para un campo estático, en el constructor estático de la clase que contiene la declaración de campo.
Estos son también los únicos contextos en los que es válido pasar un readonly campo como un out
parámetro o ref .
Si se intenta asignar a un readonly campo o pasarlo como un out ref parámetro o en cualquier otro
contexto, se trata de un error en tiempo de compilación.
Usar campos de solo lectura estáticos para constantes
Un static readonly campo es útil cuando se desea un nombre simbólico para un valor constante, pero cuando
no se permite el tipo del valor en una const declaración, o cuando el valor no se puede calcular en tiempo de
compilación. En el ejemplo
los Black miembros,, White Red , Green y Blue no se pueden declarar como const miembros porque sus
valores no se pueden calcular en tiempo de compilación. Sin embargo, si se declaran static readonly en su
lugar, se produce el mismo efecto.
Control de versiones de constantes y campos de solo lectura estáticos
Las constantes y los campos de solo lectura tienen una semántica de versiones binaria diferente. Cuando una
expresión hace referencia a una constante, el valor de la constante se obtiene en tiempo de compilación, pero
cuando una expresión hace referencia a un campo de solo lectura, el valor del campo no se obtiene hasta el
tiempo de ejecución. Considere una aplicación que consta de dos programas independientes:
using System;
namespace Program1
{
public class Utils
{
public static readonly int X = 1;
}
}
namespace Program2
{
class Test
{
static void Main() {
Console.WriteLine(Program1.Utils.X);
}
}
}
Los Program1 Program2 espacios de nombres y denotan dos programas que se compilan por separado. Dado
Program1.Utils.X que se declara como un campo estático de solo lectura, la salida del valor de la
Console.WriteLine instrucción no se conoce en tiempo de compilación, sino que se obtiene en tiempo de
ejecución. Por lo tanto, si el valor de X se cambia y Program1 se vuelve a compilar, la Console.WriteLine
instrucción generará el nuevo valor aunque no se vuelva a Program2 compilar. Sin embargo, había X sido una
constante, el valor de X se habría obtenido en el momento en Program2 que se compiló y no se vería afectado
por los cambios en Program1 hasta que se vuelva a Program2 compilar.
Campos volátiles
Cuando una field_declaration incluye un volatile modificador, los campos introducidos por esa declaración
son campos volátiles .
En el caso de los campos no volátiles, las técnicas de optimización que reordenan las instrucciones pueden
provocar resultados inesperados e imprevisibles en programas multiproceso que tienen acceso a campos sin
sincronización, como los que proporciona el lock_statement (la instrucción lock). Estas optimizaciones las puede
realizar el compilador, el sistema en tiempo de ejecución o el hardware. En el caso de los campos volátiles, las
optimizaciones de reordenación están restringidas:
Una lectura de un campo volátil se denomina lectura volátil . Una lectura volátil tiene la "semántica de
adquisición"; es decir, se garantiza que se produce antes de las referencias a la memoria que se producen
después de ella en la secuencia de instrucciones.
La escritura de un campo volátil se denomina escritura volátil . Una escritura volátil tiene "semántica de
versión"; es decir, se garantiza que se produce después de cualquier referencia de memoria antes de la
instrucción de escritura en la secuencia de instrucciones.
Estas restricciones aseguran que todos los subprocesos observarán las operaciones de escritura volátiles
realizadas por cualquier otro subproceso en el orden en que se realizaron. No se requiere una implementación
compatible para proporcionar una ordenación total única de escrituras volátiles, como se aprecia en todos los
subprocesos de ejecución. El tipo de un campo volátil debe ser uno de los siguientes:
Reference_type.
Tipo byte , sbyte , short , ushort , int , uint , char ,,, float bool System.IntPtr o System.UIntPtr .
Enum_type que tiene un tipo base enum de byte , sbyte , short , ushort , int o uint .
En el ejemplo
using System;
using System.Threading;
class Test
{
public static int result;
public static volatile bool finished;
genera el resultado:
result = 143
En este ejemplo, el método Main inicia un nuevo subproceso que ejecuta el método Thread2 . Este método
almacena un valor en un campo no volátil denominado result y, a continuación, almacena true en el campo
volatile finished . El subproceso principal espera a que el campo finished se establezca en true y, a
continuación, lee el campo result . Dado que se ha finished declarado volatile , el subproceso principal
debe leer el valor 143 del campo result . Si finished no se ha declarado el campo volatile , se permite que
el almacén result sea visible para el subproceso principal después del almacén en finished y, por lo tanto,
para que el subproceso principal Lea el valor 0 del campo result . Declarar finished como un volatile
campo evita cualquier incoherencia.
Inicialización de campos
El valor inicial de un campo, ya sea un campo estático o un campo de instancia, es el valor predeterminado
(valores predeterminados) del tipo de campo. No es posible observar el valor de un campo antes de que se haya
producido esta inicialización predeterminada y, por tanto, un campo nunca es "no inicializado". En el ejemplo
using System;
class Test
{
static bool b;
int i;
genera el resultado
b = False, i = 0
using System;
class Test
{
static double x = Math.Sqrt(2.0);
int i = 100;
string s = "Hello";
genera el resultado
Dado que una asignación x tiene lugar cuando los inicializadores de campo estáticos ejecutan y se asignan a
i y s se producen cuando se ejecutan los inicializadores de campo de instancia.
La inicialización del valor predeterminado que se describe en inicialización de campos se produce para todos los
campos, incluidos los campos que tienen inicializadores variables. Por lo tanto, cuando se inicializa una clase,
todos los campos estáticos de esa clase se inicializan primero a sus valores predeterminados y, a continuación,
los inicializadores de campo estáticos se ejecutan en orden textual. Del mismo modo, cuando se crea una
instancia de una clase, todos los campos de instancia de esa instancia se inicializan por primera vez con sus
valores predeterminados y, a continuación, los inicializadores de campo de instancia se ejecutan en orden
textual.
Es posible que se respeten los campos estáticos con inicializadores variables en su estado de valor
predeterminado. Sin embargo, esto no se recomienda como cuestión de estilo. En el ejemplo
using System;
class Test
{
static int a = b + 1;
static int b = a + 1;
exhibe este comportamiento. A pesar de las definiciones circulares de a y b, el programa es válido. Da como
resultado la salida
a = 1, b = 2
Dado que los campos estáticos a y b se inicializan en 0 (el valor predeterminado para int ) antes de que se
ejecuten sus inicializadores. Cuando se ejecuta el inicializador de a , el valor de b es cero y, por tanto, a se
inicializa en 1 . Cuando el inicializador de b se ejecuta, el valor de a ya es 1 y b se inicializa en 2 .
Inicialización de campos estáticos
Los inicializadores de variables de campo estáticos de una clase corresponden a una secuencia de asignaciones
que se ejecutan en el orden textual en el que aparecen en la declaración de clase. Si un constructor estático
(constructores estáticos) existe en la clase, la ejecución de los inicializadores de campo estáticos se produce
inmediatamente antes de ejecutar ese constructor estático. De lo contrario, los inicializadores de campo
estáticos se ejecutan en un momento dependiente de la implementación antes del primer uso de un campo
estático de esa clase. En el ejemplo
using System;
class Test
{
static void Main() {
Console.WriteLine("{0} {1}", B.Y, A.X);
}
class A
{
public static int X = Test.F("Init A");
}
class B
{
public static int Y = Test.F("Init B");
}
o la salida:
Init B
Init A
1 1
Dado que la ejecución del X inicializador de y Y del inicializador de se puede producir en cualquier orden; solo
se restringen para que se hagan referencia a esos campos. Sin embargo, en el ejemplo:
using System;
class Test
{
static void Main() {
Console.WriteLine("{0} {1}", B.Y, A.X);
}
class A
{
static A() {}
class B
{
static B() {}
Init B
Init A
1 1
Dado que las reglas para cuando los constructores estáticos se ejecutan (como se definen en constructores
estáticos), proporcionan el B constructor estático de (y, por tanto, los B inicializadores de campo estáticos)
que se deben ejecutar antes que los A inicializadores de campo y constructores estáticos de.
Inicialización de campos de instancia
Los inicializadores de variable de campo de instancia de una clase corresponden a una secuencia de
asignaciones que se ejecutan inmediatamente después de la entrada a cualquiera de los constructores de
instancia (inicializadores de constructor) de esa clase. Los inicializadores de variable se ejecutan en el orden
textual en el que aparecen en la declaración de clase. El proceso de creación e inicialización de instancias de
clase se describe con más detalle en constructores de instancias.
Un inicializador de variable para un campo de instancia no puede hacer referencia a la instancia que se está
creando. Por lo tanto, se trata de un error en tiempo de compilación para hacer referencia a this un
inicializador de variable, ya que se trata de un error en tiempo de compilación para que un inicializador de
variable haga referencia a un miembro de instancia a través de un simple_name. En el ejemplo
class A
{
int x = 1;
int y = x + 1; // Error, reference to instance member of this
}
el inicializador de variable para y genera un error en tiempo de compilación porque hace referencia a un
miembro de la instancia que se va a crear.
Métodos
Un *método _ es un miembro que implementa un cálculo o una acción que puede realizar un objeto o una
clase. Los métodos se declaran mediante _method_declaration * s:
method_declaration
: method_header method_body
;
method_header
: attributes? method_modifier* 'partial'? return_type member_name type_parameter_list?
'(' formal_parameter_list? ')' type_parameter_constraints_clause*
;
method_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'static'
| 'virtual'
| 'sealed'
| 'override'
| 'abstract'
| 'extern'
| 'async'
| method_modifier_unsafe
;
return_type
: type
| 'void'
;
member_name
: identifier
| interface_type '.' identifier
;
method_body
: block
| '=>' expression ';'
| ';'
;
Un method_declaration puede incluir un conjunto de atributos (atributos) y una combinación válida de los
cuatro modificadores de acceso (modificadores de acceso), el new (el nuevo modificador), static (métodos
estáticos y de instancia), virtual (métodos virtuales), override (métodos de invalidación), (métodos sellados),
(métodos sealed abstract abstractos) y extern Modificadores (métodos externos).
Una declaración tiene una combinación válida de modificadores si se cumplen todas las condiciones siguientes:
La Declaración incluye una combinación válida de modificadores de acceso (modificadores de acceso).
La declaración no incluye el mismo modificador varias veces.
La Declaración incluye como máximo uno de los siguientes modificadores: static , virtual y override .
La Declaración incluye como máximo uno de los siguientes modificadores: new y override .
Si la Declaración incluye el abstract modificador, la declaración no incluye ninguno de los modificadores
siguientes: static , virtual sealed o extern .
Si la Declaración incluye el private modificador, la declaración no incluye ninguno de los modificadores
siguientes: virtual , override o abstract .
Si la Declaración incluye el sealed modificador, la Declaración también incluye el override modificador.
Si la Declaración incluye el partial modificador, no incluye ninguno de los siguientes modificadores: new ,
public , protected , internal , private , virtual , sealed ,, override abstract o extern .
Un método que tiene el async modificador es una función asincrónica y sigue las reglas descritas en funciones
asincrónicas.
El return_type de una declaración de método especifica el tipo del valor calculado y devuelto por el método. El
return_type es void si el método no devuelve un valor. Si la Declaración incluye el partial modificador, el tipo
de valor devuelto debe ser void .
El member_name especifica el nombre del método. A menos que el método sea una implementación explícita
de un miembro de interfaz (implementaciones explícitas de miembros de interfaz), el member_name es
simplemente un identificador. En el caso de una implementación explícita de un miembro de interfaz, el
member_name se compone de un interface_type seguido de un " . " y un identificador.
El type_parameter_list opcional especifica los parámetros de tipo del método (parámetros de tipo). Si se
especifica un type_parameter_list el método es un *método genérico _. Si el método tiene un extern
modificador, no se puede especificar un _type_parameter_list *.
El formal_parameter_list opcional especifica los parámetros del método (parámetros de método).
Los type_parameter_constraints_clause opcionales especifican restricciones en parámetros de tipo individuales
(restricciones de parámetro de tipo) y solo se pueden especificar si también se proporciona un
type_parameter_list y el método no tiene un override modificador.
El return_type y cada uno de los tipos a los que se hace referencia en el formal_parameter_list de un método
deben ser al menos tan accesibles como el propio método (restricciones de accesibilidad).
La method_body es un punto y coma, un cuerpo de instrucción * o un cuerpo de expresión. Un cuerpo de
instrucción consta de un _block *, que especifica las instrucciones que se ejecutarán cuando se invoque el
método. Un cuerpo de expresión consta de => seguido de una expresión y un punto y coma, y denota una
expresión única que se debe realizar cuando se invoca el método.
Para abstract extern los métodos y, el method_body consta simplemente de un punto y coma. En el caso de
los partial métodos, el method_body puede constar de un punto y coma, un cuerpo de bloque o un cuerpo de
expresión. En el caso de todos los demás métodos, el method_body es un cuerpo de bloque o un cuerpo de
expresión.
Si el method_body consta de un punto y coma, la declaración no puede incluir el async modificador.
El nombre, la lista de parámetros de tipo y la lista de parámetros formales de un método definen la firma
(firmas y sobrecarga) del método. En concreto, la firma de un método consta de su nombre, el número de
parámetros de tipo y el número, modificadores y tipos de sus parámetros formales. Para estos propósitos,
cualquier parámetro de tipo del método que se produce en el tipo de un parámetro formal se identifica no por
su nombre, sino por su posición ordinal en la lista de argumentos de tipo del método. El tipo de valor devuelto
no forma parte de la signatura de un método, ni tampoco los nombres de los parámetros de tipo o los
parámetros formales.
El nombre de un método debe ser distinto de los nombres de todos los demás métodos que no se declaran en
la misma clase. Además, la firma de un método debe ser diferente de las firmas de todos los demás métodos
declarados en la misma clase y dos métodos declarados en la misma clase no pueden tener firmas que solo
difieran en ref y out .
Los type_parameter s del método están en el ámbito de la method_declaration y se pueden usar para formar
tipos en ese ámbito en return_type, method_body y type_parameter_constraints_clause s, pero no en atributos.
Todos los parámetros formales y los parámetros de tipo deben tener nombres diferentes.
Parámetros de método
Los parámetros de un método, si los hay, se declaran mediante el formal_parameter_list del método.
formal_parameter_list
: fixed_parameters
| fixed_parameters ',' parameter_array
| parameter_array
;
fixed_parameters
: fixed_parameter (',' fixed_parameter)*
;
fixed_parameter
: attributes? parameter_modifier? type identifier default_argument?
;
default_argument
: '=' expression
;
parameter_modifier
: 'ref'
| 'out'
| 'this'
;
parameter_array
: attributes? 'params' array_type identifier
;
La lista de parámetros formales está formada por uno o varios parámetros separados por comas, de los cuales
solo el último puede ser un parameter_array.
Un fixed_parameter consta de un conjunto opcional de atributos (atributos), un ref modificador opcional, out
o this , un tipo, un identificador y un default_argument opcional. Cada fixed_parameter declara un parámetro
del tipo especificado con el nombre especificado. El this modificador designa el método como un método de
extensión y solo se permite en el primer parámetro de un método estático. Los métodos de extensión se
describen con más detalle en métodos de extensión.
Un fixed_parameter con un default_argument se conoce como un *parámetro opcional , mientras que un
fixed_parameter * sin default_argument es un parámetro obligatorio *. Es posible que un parámetro necesario
no aparezca después de un parámetro opcional en un _formal_parameter_list *.
Un ref out parámetro o no puede tener un default_argument. La expresión de un default_argument debe ser
una de las siguientes:
a constant_expression
una expresión con el formato, new S() donde S es un tipo de valor.
una expresión con el formato, default(S) donde S es un tipo de valor.
La expresión debe poder convertirse implícitamente mediante una identidad o una conversión que acepte
valores NULL en el tipo del parámetro.
Si se producen parámetros opcionales en una declaración de método parcial de implementación (métodos
parciales), una implementación explícita de un miembro de interfaz (implementaciones de miembros de interfaz
explícitos) o en una declaración de indexador de un solo parámetro (indizadores), el compilador debe
proporcionar una advertencia, ya que estos miembros nunca se pueden invocar de forma que permita omitir los
argumentos.
Un parameter_array consta de un conjunto opcional de atributos (atributos), un params modificador, un
array_type y un identificador. Una matriz de parámetros declara un parámetro único del tipo de matriz
especificado con el nombre especificado. El array_type de una matriz de parámetros debe ser un tipo de matriz
unidimensional (tipos de matriz). En una invocación de método, una matriz de parámetros permite especificar
un único argumento del tipo de matriz especificado o permite que se especifiquen cero o más argumentos del
tipo de elemento de matriz. Las matrices de parámetros se describen con más detalle en matrices de
parámetros.
Una parameter_array puede producirse después de un parámetro opcional, pero no puede tener un valor
predeterminado, la omisión de los argumentos de un parameter_array daría lugar a la creación de una matriz
vacía.
En el ejemplo siguiente se muestran diferentes tipos de parámetros:
public void M(
ref int i,
decimal d,
bool b = false,
bool? n = false,
string s = "Hello",
object o = null,
T t = default(T),
params int[] a
) { }
Como se describe en firmas y sobrecarga, los ref out modificadores y forman parte de la firma de un
método, pero el params modificador no es.
Parámetros de valor
Un parámetro declarado sin modificadores es un parámetro de valor. Un parámetro de valor corresponde a una
variable local que obtiene su valor inicial del argumento correspondiente proporcionado en la invocación del
método.
Cuando un parámetro formal es un parámetro de valor, el argumento correspondiente en una invocación de
método debe ser una expresión que se pueda convertir implícitamente (conversiones implícitas) en el tipo de
parámetro formal.
Un método puede asignar nuevos valores a un parámetro de valor. Dichas asignaciones solo afectan a la
ubicación de almacenamiento local representada por el parámetro de valor; no tienen ningún efecto en el
argumento real proporcionado en la invocación del método.
Parámetros de referencia
Un parámetro declarado con un ref modificador es un parámetro de referencia. A diferencia de un parámetro
de valor, un parámetro de referencia no crea una nueva ubicación de almacenamiento. En su lugar, un parámetro
de referencia representa la misma ubicación de almacenamiento que la variable proporcionada como
argumento en la invocación del método.
Cuando un parámetro formal es un parámetro de referencia, el argumento correspondiente en una invocación
de método debe constar de la palabra clave ref seguida de un variable_reference (reglas precisas para
determinar la asignación definitiva) del mismo tipo que el parámetro formal. Una variable debe estar asignada
definitivamente antes de que se pueda pasar como parámetro de referencia.
Dentro de un método, un parámetro de referencia se considera siempre asignado definitivamente.
Un método declarado como iterador (iteradores) no puede tener parámetros de referencia.
En el ejemplo
using System;
class Test
{
static void Swap(ref int x, ref int y) {
int temp = x;
x = y;
y = temp;
}
genera el resultado
i = 2, j = 1
Para la invocación de Swap en Main , x representa i y y representa j . Por lo tanto, la invocación tiene el
efecto de intercambiar los valores de i y j .
En un método que toma parámetros de referencia, es posible que varios nombres representen la misma
ubicación de almacenamiento. En el ejemplo
class A
{
string s;
void G() {
F(ref s, ref s);
}
}
la invocación de F en G pasa una referencia a s para a y b . Por lo tanto, para esa invocación, los nombres
s , a y b hacen referencia a la misma ubicación de almacenamiento, y las tres asignaciones modifican el
campo de instancia s .
Parámetros de salida
Un parámetro declarado con un out modificador es un parámetro de salida. De forma similar a un parámetro
de referencia, un parámetro de salida no crea una nueva ubicación de almacenamiento. En su lugar, un
parámetro de salida representa la misma ubicación de almacenamiento que la variable proporcionada como
argumento en la invocación del método.
Cuando un parámetro formal es un parámetro de salida, el argumento correspondiente en una invocación de
método debe constar de la palabra clave out seguida de un variable_reference (reglas precisas para determinar
la asignación definitiva) del mismo tipo que el parámetro formal. No es necesario asignar definitivamente una
variable antes de que se pueda pasar como parámetro de salida, pero después de una invocación en la que se
pasó una variable como parámetro de salida, la variable se considera definitivamente asignada.
Dentro de un método, al igual que una variable local, un parámetro de salida se considera inicialmente sin
asignar y debe asignarse definitivamente antes de usar su valor.
Todos los parámetros de salida de un método se deben asignar definitivamente antes de que el método
devuelva.
Un método declarado como método parcial (métodos parciales) o un iterador (iteradores) no puede tener
parámetros de salida.
Los parámetros de salida se usan normalmente en métodos que producen varios valores devueltos. Por
ejemplo:
using System;
class Test
{
static void SplitPath(string path, out string dir, out string name) {
int i = path.Length;
while (i > 0) {
char ch = path[i - 1];
if (ch == '\\' || ch == '/' || ch == ':') break;
i--;
}
dir = path.Substring(0, i);
name = path.Substring(i);
}
c:\Windows\System\
hello.txt
Tenga en cuenta que las dir name variables y se pueden desasignar antes de que se pasen a SplitPath , y que
se consideran definitivamente asignadas después de la llamada.
Matrices de parámetros
Un parámetro declarado con un params modificador es una matriz de parámetros. Si una lista de parámetros
formales incluye una matriz de parámetros, debe ser el último parámetro de la lista y debe ser de un tipo de
matriz unidimensional. Por ejemplo, los tipos string[] y string[][] se pueden utilizar como el tipo de una
matriz de parámetros, pero el tipo string[,] no puede. No es posible combinar el params modificador con los
modificadores ref y out .
Una matriz de parámetros permite especificar argumentos de una de estas dos maneras en una invocación de
método:
El argumento dado para una matriz de parámetros puede ser una expresión única que se pueda convertir
implícitamente (conversiones implícitas) en el tipo de matriz de parámetros. En este caso, la matriz de
parámetros actúa exactamente como un parámetro de valor.
Como alternativa, la invocación puede especificar cero o más argumentos para la matriz de parámetros,
donde cada argumento es una expresión que se puede convertir implícitamente (conversiones implícitas) en
el tipo de elemento de la matriz de parámetros. En este caso, la invocación crea una instancia del tipo de
matriz de parámetros con una longitud correspondiente al número de argumentos, inicializa los elementos
de la instancia de la matriz con los valores de argumento especificados y utiliza la instancia de matriz recién
creada como argumento real.
A excepción de permitir un número variable de argumentos en una invocación, una matriz de parámetros es
exactamente equivalente a un parámetro de valor (parámetros de valor) del mismo tipo.
En el ejemplo
using System;
class Test
{
static void F(params int[] args) {
Console.Write("Array contains {0} elements:", args.Length);
foreach (int i in args)
Console.Write(" {0}", i);
Console.WriteLine();
}
genera el resultado
Al realizar la resolución de sobrecarga, un método con una matriz de parámetros puede ser aplicable en su
forma normal o en su forma expandida (miembro de función aplicable). La forma expandida de un método solo
está disponible si la forma normal del método no es aplicable y solo si un método aplicable con la misma
signatura que la forma expandida no se ha declarado en el mismo tipo.
En el ejemplo
using System;
class Test
{
static void F(params object[] a) {
Console.WriteLine("F(object[])");
}
genera el resultado
F();
F(object[]);
F(object,object);
F(object[]);
F(object[]);
En el ejemplo, dos de las formas expandidas posibles del método con una matriz de parámetros ya están
incluidas en la clase como métodos normales. Por lo tanto, estos formularios expandidos no se tienen en cuenta
al realizar la resolución de sobrecarga, y las invocaciones del método primero y tercer, por tanto, seleccionan los
métodos normales. Cuando una clase declara un método con una matriz de parámetros, no es raro incluir
también algunos de los formularios expandidos como métodos normales. Al hacerlo, es posible evitar la
asignación de una instancia de matriz que se produce cuando se invoca una forma expandida de un método con
una matriz de parámetros.
Cuando el tipo de una matriz de parámetros es object[] , se produce una ambigüedad potencial entre la forma
normal del método y la forma empleada para un solo object parámetro. La razón de la ambigüedad es que,
object[] a su vez, se pueda convertir implícitamente al tipo object . Sin embargo, la ambigüedad no presenta
ningún problema, ya que se puede resolver mediante la inserción de una conversión si es necesario.
En el ejemplo
using System;
class Test
{
static void F(params object[] args) {
foreach (object o in args) {
Console.Write(o.GetType().FullName);
Console.Write(" ");
}
Console.WriteLine();
}
genera el resultado
En la primera y última invocación de F , la forma normal de F es aplicable porque existe una conversión
implícita desde el tipo de argumento al tipo de parámetro (ambos son del tipo object[] ). Por lo tanto, la
resolución de sobrecarga selecciona la forma normal de F y el argumento se pasa como un parámetro de valor
normal. En la segunda y tercera invocación, la forma normal de F no es aplicable porque no existe ninguna
conversión implícita desde el tipo de argumento al tipo de parámetro (el tipo object no se puede convertir
implícitamente al tipo object[] ). Sin embargo, la forma expandida de F es aplicable, por lo que está
seleccionada por la resolución de sobrecarga. Como resultado, object[] la invocación crea un elemento, y el
único elemento de la matriz se inicializa con el valor de argumento dado (que a su vez es una referencia a un
object[] ).
Para cada método virtual declarado en o heredado por una clase, existe una implementación más derivada
del método con respecto a esa clase. La implementación más derivada de un método virtual M con respecto a
una clase R se determina de la manera siguiente:
Si R contiene la virtual declaración de introducción de M , ésta es la implementación más derivada de M
.
De lo contrario, si R contiene una override de M , esta es la implementación más derivada de M .
De lo contrario, la implementación más derivada de M con respecto a R es la misma que la
implementación más derivada de M con respecto a la clase base directa de R .
En el ejemplo siguiente se muestran las diferencias entre los métodos virtuales y los no virtuales:
using System;
class A
{
public void F() { Console.WriteLine("A.F"); }
class B: A
{
new public void F() { Console.WriteLine("B.F"); }
class Test
{
static void Main() {
B b = new B();
A a = b;
a.F();
b.F();
a.G();
b.G();
}
}
A.F
B.F
B.G
B.G
Observe que la instrucción a.G() invoca B.G , no A.G . Esto se debe a que el tipo en tiempo de ejecución de la
instancia (que es B ), y no el tipo en tiempo de compilación de la instancia (que es A ), determina la
implementación de método real que se va a invocar.
Dado que los métodos pueden ocultar métodos heredados, es posible que una clase contenga varios métodos
virtuales con la misma firma. Esto no presenta un problema de ambigüedad, ya que todo menos el método más
derivado está oculto. En el ejemplo
using System;
class A
{
public virtual void F() { Console.WriteLine("A.F"); }
}
class B: A
{
public override void F() { Console.WriteLine("B.F"); }
}
class C: B
{
new public virtual void F() { Console.WriteLine("C.F"); }
}
class D: C
{
public override void F() { Console.WriteLine("D.F"); }
}
class Test
{
static void Main() {
D d = new D();
A a = d;
B b = d;
C c = d;
a.F();
b.F();
c.F();
d.F();
}
}
las C D clases y contienen dos métodos virtuales con la misma firma: los introducidos por A y los
introducidos por C . El método introducido por C oculta el método heredado de A . Por lo tanto, la
declaración de invalidación en D invalida el método introducido por C , y no es posible D que invalide el
método introducido por A . En el ejemplo se genera el resultado:
B.F
B.F
D.F
D.F
Tenga en cuenta que es posible invocar el método virtual oculto mediante el acceso a una instancia de D a
través de un tipo menos derivado en el que el método no está oculto.
Métodos de invalidación
Cuando una declaración de método de instancia incluye un override modificador, se dice que el método es un
método de invalidación . Un método de invalidación invalida un método virtual heredado con la misma firma.
Mientras que una declaración de método virtual introduce un método nuevo, una declaración de método de
reemplazo especializa un método virtual heredado existente proporcionando una nueva implementación de ese
método.
El método invalidado por una override declaración se conoce como el método base invalidado . Para un
método de invalidación M declarado en una clase C , el método base invalidado se determina examinando
cada tipo de clase base de C , empezando por el tipo de clase base directa de C y continuando con cada tipo
de clase base directo sucesivo, hasta que en un tipo de clase base determinado se encuentra un método
accesible que tiene la misma firma que M después de la sustitución de los argumentos de tipo. Con el fin de
localizar el método base invalidado, se considera que un método es accesible si es, si es, si es, public
protected protected internal o si es y se internal declara en el mismo programa que C .
Se produce un error en tiempo de compilación a menos que se cumplan todas las condiciones siguientes para
una declaración de invalidación:
Un método base invalidado se puede encontrar como se describió anteriormente.
Hay exactamente un método base invalidado de este tipo. Esta restricción solo tiene efecto si el tipo de clase
base es un tipo construido en el que la sustitución de los argumentos de tipo hace que la firma de dos
métodos sea la misma.
El método base invalidado es un método virtual, abstracto o de invalidación. En otras palabras, el método
base invalidado no puede ser estático o no virtual.
El método base invalidado no es un método sellado.
El método de invalidación y el método base invalidado tienen el mismo tipo de valor devuelto.
La declaración de invalidación y el método base invalidado tienen la misma accesibilidad declarada. En otras
palabras, una declaración de invalidación no puede cambiar la accesibilidad del método virtual. Sin embargo,
si el método base invalidado es interno protegido y se declara en un ensamblado diferente al del
ensamblado que contiene el método de invalidación, se debe proteger la accesibilidad declarada del método
de invalidación.
La declaración de invalidación no especifica las cláusulas-de-parámetros de tipo. En su lugar, las restricciones
se heredan del método base invalidado. Tenga en cuenta que las restricciones que son parámetros de tipo en
el método invalidado se pueden reemplazar por argumentos de tipo en la restricción heredada. Esto puede
dar lugar a restricciones que no son válidas cuando se especifican explícitamente, como tipos de valor o tipos
sellados.
En el ejemplo siguiente se muestra cómo funcionan las reglas de reemplazo para las clases genéricas:
class D: C<string>
{
public override string F() {...} // Ok
public override C<string> G() {...} // Ok
public override void H(C<T> x) {...} // Error, should be C<string>
}
Una declaración de invalidación puede tener acceso al método base invalidado mediante un base_access (acceso
base). En el ejemplo
class A
{
int x;
class B: A
{
int y;
Solo si se incluye un override modificador, un método puede reemplazar a otro método. En todos los demás
casos, un método con la misma signatura que un método heredado simplemente oculta el método heredado. En
el ejemplo
class A
{
public virtual void F() {}
}
class B: A
{
public virtual void F() {} // Warning, hiding inherited F()
}
class A
{
public virtual void F() {}
}
class B: A
{
new private void F() {} // Hides A.F within body of B
}
class C: B
{
public override void F() {} // Ok, overrides A.F
}
el F método en B oculta el método virtual F heredado de A . Dado que el nuevo F en B tiene acceso
privado, su ámbito solo incluye el cuerpo de clase de B y no se extiende a C . Por consiguiente, la declaración
de F en puede C invalidar el F heredado de A .
Métodos sellados
Cuando una declaración de método de instancia incluye un sealed modificador, se dice que se trata de un
método sellado . Si una declaración de método de instancia incluye el sealed modificador, también debe
incluir el override modificador. El uso del sealed modificador impide que una clase derivada Reemplace el
método.
En el ejemplo
using System;
class A
{
public virtual void F() {
Console.WriteLine("A.F");
}
class B: A
{
sealed override public void F() {
Console.WriteLine("B.F");
}
class C: B
{
override public void G() {
Console.WriteLine("C.G");
}
}
la clase B proporciona dos métodos de invalidación: un F método que tiene el sealed modificador y un G
método que no lo hace. B el uso del sellado modifier evita que se C invalide más F .
Métodos abstractos
Cuando una declaración de método de instancia incluye un abstract modificador, se dice que el método es un
método abstracto . Aunque un método abstracto también es implícitamente un método virtual, no puede tener
el modificador virtual .
Una declaración de método abstracto presenta un nuevo método virtual, pero no proporciona una
implementación de ese método. En su lugar, las clases derivadas no abstractas deben proporcionar su propia
implementación mediante la invalidación de ese método. Dado que un método abstracto no proporciona
ninguna implementación real, el method_body de un método abstracto simplemente consiste en un punto y
coma.
Las declaraciones de método abstracto solo se permiten en clases abstractas (clases abstractas).
En el ejemplo
public abstract class Shape
{
public abstract void Paint(Graphics g, Rectangle r);
}
la Shapeclase define la noción abstracta de un objeto de forma geométrica que puede dibujarse a sí mismo. El
Paint método es abstracto porque no hay ninguna implementación predeterminada significativa. Las Ellipse
Box clases y son Shape implementaciones concretas. Dado que estas clases no son abstractas, son necesarias
para invalidar el Paint método y proporcionar una implementación real.
Se trata de un error en tiempo de compilación para que un base_access (acceso base) haga referencia a un
método abstracto. En el ejemplo
abstract class A
{
public abstract void F();
}
class B: A
{
public override void F() {
base.F(); // Error, base.F is abstract
}
}
se ha detectado un error en tiempo de compilación para la base.F() invocación porque hace referencia a un
método abstracto.
Se permite que una declaración de método abstracto invalide un método virtual. Esto permite a una clase
abstracta forzar la reimplementación del método en las clases derivadas y hace que la implementación original
del método no esté disponible. En el ejemplo
using System;
class A
{
public virtual void F() {
Console.WriteLine("A.F");
}
}
abstract class B: A
{
public abstract override void F();
}
class C: B
{
public override void F() {
Console.WriteLine("C.F");
}
}
A la clase declara un método virtual, B la clase invalida este método con un método abstracto y la clase C
invalida el método abstracto para proporcionar su propia implementación.
Métodos externos
Cuando una declaración de método incluye un extern modificador, se dice que el método es un *método
externo _. Los métodos externos se implementan externamente, normalmente con un lenguaje distinto de C#.
Dado que una declaración de método externo no proporciona ninguna implementación real, el _method_body *
de un método externo consiste simplemente en un punto y coma. Es posible que un método externo no sea
genérico.
El extern modificador se usa normalmente junto con un DllImport atributo (interoperación con componentes
com y Win32), lo que permite que los métodos externos se implementen mediante archivos DLL (bibliotecas de
vínculos dinámicos). El entorno de ejecución puede admitir otros mecanismos en los que se puedan
proporcionar implementaciones de métodos externos.
Cuando un método externo incluye un DllImport atributo, la declaración del método también debe incluir un
static modificador. En este ejemplo se muestra el uso del extern modificador y el DllImport atributo:
using System.Text;
using System.Security.Permissions;
using System.Runtime.InteropServices;
class Path
{
[DllImport("kernel32", SetLastError=true)]
static extern bool CreateDirectory(string name, SecurityAttribute sa);
[DllImport("kernel32", SetLastError=true)]
static extern bool RemoveDirectory(string name);
[DllImport("kernel32", SetLastError=true)]
static extern int GetCurrentDirectory(int bufSize, StringBuilder buf);
[DllImport("kernel32", SetLastError=true)]
static extern bool SetCurrentDirectory(string name);
}
public static T[] Slice<T>(this T[] source, int index, int count) {
if (index < 0 || count < 0 || source.Length - index < count)
throw new ArgumentException();
T[] result = new T[count];
Array.Copy(source, index, result, 0, count);
return result;
}
}
Un método de extensión es un método estático normal. Además, cuando la clase estática envolvente está en el
ámbito, se puede invocar un método de extensión mediante la sintaxis de invocación de método de instancia
(invocaciones de método de extensión), mediante la expresión de receptor como primer argumento.
El programa siguiente utiliza los métodos de extensión declarados anteriormente:
El Slice método está disponible en string[] , y el ToInt32 método está disponible en string , porque se
han declarado como métodos de extensión. El significado del programa es el mismo que el que se indica a
continuación, mediante llamadas ordinarias al método estático:
class A
{
public int F() {} // Error, return value required
el método que devuelve el valor F produce un error en tiempo de compilación porque el control puede fluir
fuera del final del cuerpo del método. Los G H métodos y son correctos porque todas las rutas de acceso de
ejecución posibles terminan en una instrucción return que especifica un valor devuelto. El I método es
correcto porque su cuerpo es equivalente a un bloque de instrucciones con solo una única instrucción return en
él.
Sobrecarga de métodos
Las reglas de resolución de sobrecarga de método se describen en inferencia de tipos.
Propiedades
Una *propiedad _ es un miembro que proporciona acceso a una característica de un objeto o una clase. Entre
los ejemplos de propiedades se incluyen la longitud de una cadena, el tamaño de una fuente, el título de una
ventana, el nombre de un cliente, etc. Las propiedades son una extensión natural de los campos: ambos son
miembros con nombre con tipos asociados y la sintaxis para tener acceso a campos y propiedades es la misma.
Sin embargo, a diferencia de los campos, las propiedades no denotan ubicaciones de almacenamiento. En su
lugar, las propiedades tienen _ descriptores de acceso* que especifican las instrucciones que se ejecutarán
cuando se lean o escriban sus valores. Por tanto, las propiedades proporcionan un mecanismo para asociar
acciones con la lectura y escritura de los atributos de un objeto; Además, permiten calcular tales atributos.
Las propiedades se declaran mediante property_declaration s:
property_declaration
: attributes? property_modifier* type member_name property_body
;
property_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'static'
| 'virtual'
| 'sealed'
| 'override'
| 'abstract'
| 'extern'
| property_modifier_unsafe
;
property_body
: '{' accessor_declarations '}' property_initializer?
| '=>' expression ';'
;
property_initializer
: '=' variable_initializer ';'
;
Un property_declaration puede incluir un conjunto de atributos (atributos) y una combinación válida de los
cuatro modificadores de acceso (modificadores de acceso), el new (el nuevo modificador), static (métodos
estáticos y de instancia), virtual (métodos virtuales), override (métodos de invalidación), (métodos sellados),
(métodos sealed abstract abstractos) y extern Modificadores (métodos externos).
Las declaraciones de propiedad están sujetas a las mismas reglas que las declaraciones de método (métodos)
con respecto a las combinaciones válidas de modificadores.
El tipo de una declaración de propiedad especifica el tipo de la propiedad introducida por la declaración y el
member_name especifica el nombre de la propiedad. A menos que la propiedad sea una implementación
explícita de un miembro de interfaz, el member_name es simplemente un identificador. En el caso de una
implementación explícita de un miembro de interfaz (implementaciones de miembros de interfaz explícitos), el
member_name consta de un interface_type seguido de un " . " y un identificador.
El tipo de una propiedad debe ser al menos igual de accesible que la propiedad en sí (restricciones de
accesibilidad).
Un property_body puede consistir en un "*cuerpo del descriptor de acceso***" o un _cuerpo de expresión*. En el
cuerpo de un descriptor de acceso, _accessor_declarations *, que debe incluirse en los { tokens "" y "" } ,
declare los descriptores de acceso (descriptores de acceso) de la propiedad. Los descriptores de acceso
especifican las instrucciones ejecutables asociadas a la lectura y la escritura de la propiedad.
Un cuerpo de expresión que consta de => seguido de una expresión E y un punto y coma es exactamente
equivalente al cuerpo de la instrucción { get { return E; } } y, por lo tanto, solo se puede usar para especificar
propiedades de solo captador donde el resultado del captador se proporciona mediante una sola expresión.
Solo se puede proporcionar un property_initializer para una propiedad implementada automáticamente
(propiedades implementadas automáticamente) y provoca la inicialización del campo subyacente de dichas
propiedades con el valor especificado por la expresión.
Aunque la sintaxis para tener acceso a una propiedad es la misma que la de un campo, una propiedad no se
clasifica como una variable. Por lo tanto, no es posible pasar una propiedad como ref argumento o out .
Cuando una declaración de propiedad incluye un extern modificador, se dice que la propiedad es una
*propiedad externa _. Dado que una declaración de propiedad externa no proporciona ninguna
implementación real, cada una de sus _accessor_declarations * consta de un punto y coma.
Propiedades estáticas y de instancia
Cuando una declaración de propiedad incluye un static modificador, se dice que la propiedad es una
*propiedad estática _. Cuando no static existe ningún modificador, se dice que la propiedad es una
propiedad de instancia _ * * *.
Una propiedad estática no está asociada a una instancia específica y es un error en tiempo de compilación para
hacer referencia a this en los descriptores de acceso de una propiedad estática.
Una propiedad de instancia está asociada a una instancia determinada de una clase y se puede tener acceso a
esa instancia como this (este acceso) en los descriptores de acceso de esa propiedad.
Cuando se hace referencia a una propiedad en un member_access (acceso a miembros) del formulario E.M , si
M es una propiedad estática, E debe indicar un tipo que contiene M y si M es una propiedad de instancia, E
debe indicar una instancia de un tipo que contiene M .
Las diferencias entre los miembros estáticos y de instancia se tratan más adelante en miembros estáticos y de
instancia.
Descriptores de acceso
El accessor_declarations de una propiedad especifica las instrucciones ejecutables asociadas a la lectura y
escritura de esa propiedad.
accessor_declarations
: get_accessor_declaration set_accessor_declaration?
| set_accessor_declaration get_accessor_declaration?
;
get_accessor_declaration
: attributes? accessor_modifier? 'get' accessor_body
;
set_accessor_declaration
: attributes? accessor_modifier? 'set' accessor_body
;
accessor_modifier
: 'protected'
| 'internal'
| 'private'
| 'protected' 'internal'
| 'internal' 'protected'
;
accessor_body
: block
| ';'
;
el Button control declara una propiedad pública Caption . El get descriptor de acceso de la Caption
propiedad devuelve la cadena almacenada en el caption campo privado. El set descriptor de acceso
comprueba si el nuevo valor es diferente del valor actual y, en ese caso, almacena el nuevo valor y vuelve a
dibujar el control. Las propiedades suelen seguir el patrón mostrado anteriormente: el get descriptor de acceso
devuelve simplemente un valor almacenado en un campo privado y el set descriptor de acceso modifica ese
campo privado y, a continuación, realiza las acciones adicionales necesarias para actualizar completamente el
estado del objeto.
Dada la Button clase anterior, el siguiente es un ejemplo de uso de la Caption propiedad:
Aquí, el set descriptor de acceso se invoca mediante la asignación de un valor a la propiedad, y el get
descriptor de acceso se invoca mediante la referencia a la propiedad en una expresión.
Los get set descriptores de acceso y de una propiedad no son miembros distintos y no es posible declarar
los descriptores de acceso de una propiedad por separado. Como tal, no es posible que los dos descriptores de
acceso de una propiedad de lectura y escritura tengan una accesibilidad diferente. En el ejemplo
class A
{
private string name;
no declara una única propiedad de lectura y escritura. En su lugar, declara dos propiedades con el mismo
nombre, una de solo lectura y otra de solo escritura. Dado que dos miembros declarados en la misma clase no
pueden tener el mismo nombre, en el ejemplo se produce un error en tiempo de compilación.
Cuando una clase derivada declara una propiedad con el mismo nombre que una propiedad heredada, la
propiedad derivada oculta la propiedad heredada con respecto a la lectura y la escritura. En el ejemplo
class A
{
public int P {
set {...}
}
}
class B: A
{
new public int P {
get {...}
}
}
la P propiedad de B oculta la P propiedad en A con respecto a la lectura y la escritura. Por lo tanto, en las
instrucciones
B b = new B();
b.P = 1; // Error, B.P is read-only
((A)b).P = 1; // Ok, reference to A.P
la asignación a b.P produce un error en tiempo de compilación, ya que la propiedad de solo lectura P de B
oculta la propiedad de solo escritura P en A . Sin embargo, tenga en cuenta que se puede usar una conversión
para tener acceso a la P propiedad Hidden.
A diferencia de los campos públicos, las propiedades proporcionan una separación entre el estado interno de un
objeto y su interfaz pública. Observe el ejemplo:
class Label
{
private int x, y;
private string caption;
public int X {
get { return x; }
}
public int Y {
get { return y; }
}
En este caso, la Label clase usa dos int campos, x y y , para almacenar su ubicación. La ubicación se
expone públicamente como X y una Y propiedad y como Location propiedad de tipo Point . Si, en una
versión futura de Label , resulta más cómodo almacenar la ubicación Point internamente, el cambio se puede
realizar sin que afecte a la interfaz pública de la clase:
class Label
{
private Point location;
private string caption;
public int X {
get { return location.x; }
}
public int Y {
get { return location.y; }
}
Exponer el estado a través de las propiedades no es necesariamente menos eficaz que exponer los campos
directamente. En concreto, cuando una propiedad no es virtual y contiene solo una pequeña cantidad de código,
el entorno de ejecución puede reemplazar las llamadas a los descriptores de acceso con el código real de los
descriptores de acceso. Este proceso se conoce como inserción y hace que el acceso a la propiedad sea tan
eficaz como el acceso al campo, pero conserva la mayor flexibilidad de las propiedades.
Dado que la invocación get de un descriptor de acceso es conceptualmente equivalente a leer el valor de un
campo, se considera un estilo de programación incorrecto para que los get descriptores de acceso tengan
efectos secundarios observables. En el ejemplo
class Counter
{
private int next;
el valor de la Next propiedad depende del número de veces que se ha accedido previamente a la propiedad.
Por lo tanto, el acceso a la propiedad produce un efecto secundario observable y la propiedad debe
implementarse como un método en su lugar.
La Convención "sin efectos secundarios" para los get descriptores de acceso no significa que los get
descriptores de acceso siempre se deben escribir para devolver los valores almacenados en los campos. En
realidad, get los descriptores de acceso suelen calcular el valor de una propiedad mediante el acceso a varios
campos o la invocación de métodos. Sin embargo, un get descriptor de acceso diseñado correctamente no
realiza ninguna acción que produzca cambios observables en el estado del objeto.
Las propiedades se pueden usar para retrasar la inicialización de un recurso hasta el momento en el que se hace
referencia a él por primera vez. Por ejemplo:
using System.IO;
La Console clase contiene tres propiedades, In , Out y Error , que representan los dispositivos de entrada,
salida y error estándar, respectivamente. Al exponer estos miembros como propiedades, la Console clase puede
retrasar su inicialización hasta que se usen realmente. Por ejemplo, al hacer referencia por primera vez a la Out
propiedad, como en
Console.Out.WriteLine("hello, world");
TextWriter se crea el subyacente para el dispositivo de salida. Pero si la aplicación no hace referencia a las In
Error propiedades y, no se crea ningún objeto para esos dispositivos.
Propiedades implementadas automáticamente
Una propiedad implementada automáticamente (o propiedad automática para abreviar) es una propiedad no
abstracta no abstracta con cuerpos de descriptor de acceso solo de punto y coma. Las propiedades automáticas
deben tener un descriptor de acceso get y, opcionalmente, tener un descriptor de acceso set.
Cuando se especifica una propiedad como una propiedad implementada automáticamente, un campo de
respaldo oculto está disponible automáticamente para la propiedad y los descriptores de acceso se
implementan para leer y escribir en ese campo de respaldo. Si la propiedad automática no tiene ningún
descriptor de acceso set, se considera el campo de respaldo readonly (campos de solo lectura). Al igual
readonly que un campo, una propiedad automática de solo captador también se puede asignar a en el cuerpo
de un constructor de la clase envolvente. Este tipo de asignación asigna directamente al campo de respaldo de
solo lectura de la propiedad.
Una propiedad automática puede tener opcionalmente un property_initializer, que se aplica directamente al
campo de respaldo como un variable_initializer (inicializadores de variables).
En el ejemplo siguiente:
En el ejemplo siguiente:
Observe que las asignaciones al campo de solo lectura son válidas, ya que se producen dentro del constructor.
Accesibilidad
Si un descriptor de acceso tiene un accessor_modifier, el dominio de accesibilidad (dominios de accesibilidad)
del descriptor de acceso se determina mediante la accesibilidad declarada de la accessor_modifier. Si un
descriptor de acceso no tiene un accessor_modifier, el dominio de accesibilidad del descriptor de acceso se
determina a partir de la accesibilidad declarada de la propiedad o del indizador.
La presencia de un accessor_modifier nunca afecta a la búsqueda de miembros (operadores) o a la resolución
de sobrecarga (resolución de sobrecarga). Los modificadores de la propiedad o el indizador siempre determinan
la propiedad o el indizador que está enlazado, independientemente del contexto del acceso.
Una vez que se ha seleccionado una propiedad o un indizador determinados, se usan los dominios de
accesibilidad de los descriptores de acceso específicos implicados para determinar si ese uso es válido:
Si el uso es como un valor (valores de expresiones), el get descriptor de acceso debe existir y ser accesible.
Si el uso es como el destino de una asignación simple (asignación simple), el set descriptor de acceso debe
existir y ser accesible.
Si el uso es como destino de la asignación compuesta (asignación compuesta), o como destino de los ++
operadores o -- (miembros de función0,9, expresiones de invocación), los get descriptores de acceso y el
set descriptor de acceso deben existir y ser accesibles.
En el ejemplo siguiente, la propiedad A.Text oculta la propiedad B.Text , incluso en contextos donde solo
set se llama al descriptor de acceso. En cambio, la propiedad B.Count no es accesible a la clase M , por lo que
en A.Count su lugar se usa la propiedad accesible.
class A
{
public string Text {
get { return "hello"; }
set { }
}
class B: A
{
private string text = "goodbye";
private int count = 0;
class M
{
static void Main() {
B b = new B();
b.Count = 12; // Calls A.Count set accessor
int i = b.Count; // Calls A.Count get accessor
b.Text = "howdy"; // Error, B.Text set accessor not accessible
string s = b.Text; // Calls B.Text get accessor
}
}
Un descriptor de acceso que se utiliza para implementar una interfaz no puede tener un accessor_modifier. Si
solo se usa un descriptor de acceso para implementar una interfaz, el otro descriptor de acceso se puede
declarar con un accessor_modifier:
public interface I
{
string Prop { get; }
}
public class C: I
{
public string Prop {
get { return "April"; } // Must not have a modifier here
internal set {...} // Ok, because I.Prop has no set accessor
}
}
En el ejemplo
abstract class A
{
int y;
X es una propiedad virtual de solo lectura, Y es una propiedad de lectura y escritura virtual y Z es una
propiedad de lectura y escritura abstracta. Dado Z que es abstracto, la clase contenedora A también debe
declararse como abstracta.
Una clase que deriva de A se muestra a continuación:
class B: A
{
int z;
Aquí, las declaraciones de X , Y y son las Z declaraciones de propiedad de reemplazo. Cada declaración de
propiedad coincide exactamente con los modificadores de accesibilidad, el tipo y el nombre de la propiedad
heredada correspondiente. El get descriptor de acceso de X y el set descriptor de acceso de Y utilizan la
base palabra clave para tener acceso a los descriptores de acceso La declaración de Z invalida ambos
descriptores de acceso abstractos; por tanto, no hay miembros de función abstracta pendientes en y B B se
permite que sea una clase no abstracta.
Cuando una propiedad se declara como override , cualquier descriptor de acceso invalidado debe ser accesible
para el código de invalidación. Además, la accesibilidad declarada de la propiedad o del indexador, y de los
descriptores de acceso, debe coincidir con la del miembro y los descriptores de acceso invalidados. Por ejemplo:
public class B
{
public virtual int P {
protected set {...}
get {...}
}
}
public class D: B
{
public override int P {
protected set {...} // Must specify protected here
get {...} // Must not have a modifier here
}
}
Events
Un *evento _ es un miembro que permite a un objeto o una clase proporcionar notificaciones. Los clientes
pueden adjuntar código ejecutable para eventos proporcionando _ controladores de eventos *.
Los eventos se declaran mediante event_declaration s:
event_declaration
: attributes? event_modifier* 'event' type variable_declarators ';'
| attributes? event_modifier* 'event' type member_name '{' event_accessor_declarations '}'
;
event_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'static'
| 'virtual'
| 'sealed'
| 'override'
| 'abstract'
| 'extern'
| event_modifier_unsafe
;
event_accessor_declarations
: add_accessor_declaration remove_accessor_declaration
| remove_accessor_declaration add_accessor_declaration
;
add_accessor_declaration
: attributes? 'add' block
;
remove_accessor_declaration
: attributes? 'remove' block
;
Un event_declaration puede incluir un conjunto de atributos (atributos) y una combinación válida de los cuatro
modificadores de acceso (modificadores de acceso), el new (el nuevo modificador), static (métodos estáticos
y de instancia), virtual (métodos virtuales), override (métodos de invalidación), (métodos sellados), (métodos
sealed abstract abstractos) y extern Modificadores (métodos externos).
Las declaraciones de eventos están sujetas a las mismas reglas que las declaraciones de método (métodos) con
respecto a las combinaciones válidas de modificadores.
El tipo de una declaración de evento debe ser un delegate_type (tipos de referencia) y ese delegate_type debe
ser al menos igual de accesible que el propio evento (restricciones de accesibilidad).
Una declaración de evento puede incluir event_accessor_declarations. Sin embargo, si no, para los eventos no
abstractos y no externos, el compilador los proporciona automáticamente (eventos similares a los campos); en
el caso de los eventos extern, los descriptores de acceso se proporcionan externamente.
Una declaración de evento que omite event_accessor_declarations define uno o más eventos, uno para cada una
de las variable_declarator s. Los atributos y modificadores se aplican a todos los miembros declarados por este
tipo de event_declaration.
Se trata de un error en tiempo de compilación para que una event_declaration incluya tanto el abstract
modificador como el event_accessor_declarations delimitado por llaves.
Cuando una declaración de evento incluye un extern modificador, se dice que el evento es un *evento
externo . Dado que una declaración de evento externo no proporciona ninguna implementación real, es un error
que incluya tanto el extern modificador como el _event_accessor_declarations *.
Se trata de un error en tiempo de compilación para una variable_declarator de una declaración de evento con
un abstract external modificador o para incluir un variable_initializer.
Un evento se puede usar como operando izquierdo de los += -= operadores y (asignación deeventos). Estos
operadores se utilizan, respectivamente, para adjuntar controladores de eventos a o para quitar controladores
de eventos de un evento, y los modificadores de acceso del evento controlan los contextos en los que se
permiten esas operaciones.
Puesto que += y -= son las únicas operaciones que se permiten en un evento fuera del tipo que declara el
evento, el código externo puede Agregar y quitar controladores para un evento, pero no puede obtener o
modificar la lista subyacente de controladores de eventos.
En una operación de la forma x += y o x -= y , cuando x es un evento y la referencia tiene lugar fuera del
tipo que contiene la declaración de x , el resultado de la operación tiene el tipo void (en lugar de tener el tipo
de x , con el valor de x después de la asignación). Esta regla prohíbe que el código externo examine
indirectamente el delegado subyacente de un evento.
En el ejemplo siguiente se muestra cómo se adjuntan los controladores de eventos a las instancias de la Button
clase:
public delegate void EventHandler(object sender, EventArgs e);
public LoginDialog() {
OkButton = new Button(...);
OkButton.Click += new EventHandler(OkButtonClick);
CancelButton = new Button(...);
CancelButton.Click += new EventHandler(CancelButtonClick);
}
En este caso, el LoginDialog constructor de instancia crea dos Button instancias y asocia los controladores de
eventos a los Click eventos.
Eventos similares a campos
En el texto del programa de la clase o el struct que contiene la declaración de un evento, se pueden usar
determinados eventos como campos. Para usar de esta manera, un evento no debe ser abstract o extern y no
debe incluir explícitamente event_accessor_declarations. Este tipo de evento se puede utilizar en cualquier
contexto que permita un campo. El campo contiene un delegado (delegados) que hace referencia a la lista de
controladores de eventos que se han agregado al evento. Si no se ha agregado ningún controlador de eventos,
el campo contiene null .
En el ejemplo
Click se usa como un campo dentro de la Button clase. Como muestra el ejemplo, el campo se puede
examinar, modificar y usar en expresiones de invocación de delegado. El OnClick método de la Button clase
"genera" el Click evento. La noción de generar un evento es equivalente exactamente a invocar el delegado
representado por el evento; por lo tanto, no hay ninguna construcción especial de lenguaje para generar
eventos. Tenga en cuenta que la invocación del delegado está precedida por una comprobación que garantiza
que el delegado no es NULL.
Fuera de la declaración de la Button clase, el Click miembro solo se puede usar en el lado izquierdo de los
+= operadores y -= , como en
class X
{
public event D Ev;
}
class X
{
private D __Ev; // field to hold the delegate
public event D Ev {
add {
/* add the delegate in a thread safe way */
}
remove {
/* remove the delegate in a thread safe way */
}
}
}
Dentro de la clase X , las referencias a Ev en el lado izquierdo de los += -= operadores y hacen que se
invoquen los descriptores de acceso add y Remove. Todas las demás referencias a Ev se compilan para hacer
referencia al campo oculto en __Ev su lugar (acceso a miembros). El nombre " __Ev " es arbitrario; el campo
oculto puede tener cualquier nombre o ningún nombre.
Descriptores de acceso de un evento
Las declaraciones de evento normalmente omiten event_accessor_declarations, como en el Button ejemplo
anterior. Una situación para hacerlo implica el caso en el que el costo de almacenamiento de un campo por
evento no es aceptable. En tales casos, una clase puede incluir event_accessor_declarations y utilizar un
mecanismo privado para almacenar la lista de controladores de eventos.
En el event_accessor_declarations de un evento se especifican las instrucciones ejecutables asociadas a la
adición y eliminación de controladores de eventos.
Las declaraciones de descriptor de acceso constan de un add_accessor_declaration y un
remove_accessor_declaration. Cada declaración de descriptor de acceso consta del token add o remove va
seguido de un bloque. El bloque asociado a un add_accessor_declaration especifica las instrucciones que se
ejecutarán cuando se agregue un controlador de eventos y el bloque asociado a un
remove_accessor_declaration especifica las instrucciones que se ejecutarán cuando se quite un controlador de
eventos.
Cada add_accessor_declaration y remove_accessor_declaration corresponde a un método con un parámetro de
valor único del tipo de evento y un tipo de valor void devuelto. El parámetro implícito de un descriptor de
acceso de eventos se denomina value . Cuando se utiliza un evento en una asignación de eventos, se usa el
descriptor de acceso de eventos adecuado. En concreto, si el operador de asignación es += , se usa el descriptor
de acceso add y, si el operador de asignación es -= , se usa el descriptor de acceso Remove. En cualquier caso,
el operando derecho del operador de asignación se utiliza como argumento para el descriptor de acceso de
eventos. El bloque de una add_accessor_declaration o un remove_accessor_declaration debe ajustarse a las
reglas de void los métodos descritos en el cuerpo del método. En concreto, las return instrucciones de un
bloque de este tipo no pueden especificar una expresión.
Dado que un descriptor de acceso de eventos tiene implícitamente un parámetro denominado value , se trata
de un error en tiempo de compilación para una variable local o una constante declarada en un descriptor de
acceso de eventos para que tenga ese nombre.
En el ejemplo
// MouseDown event
public event MouseEventHandler MouseDown {
add { AddEventHandler(mouseDownEventKey, value); }
remove { RemoveEventHandler(mouseDownEventKey, value); }
}
// MouseUp event
public event MouseEventHandler MouseUp {
add { AddEventHandler(mouseUpEventKey, value); }
remove { RemoveEventHandler(mouseUpEventKey, value); }
}
Una abstract declaración de evento especifica que los descriptores de acceso del evento son virtuales, pero no
proporciona una implementación real de los descriptores de acceso. En su lugar, las clases derivadas no
abstractas deben proporcionar su propia implementación para los descriptores de acceso invalidando el evento.
Dado que una declaración de evento abstracto no proporciona ninguna implementación real, no puede
proporcionar event_accessor_declarations delimitados por llaves.
Una declaración de evento que incluye los abstract override modificadores y especifica que el evento es
abstracto e invalida un evento base. Los descriptores de acceso de este tipo de evento también son abstractos.
Las declaraciones de eventos abstractos solo se permiten en clases abstractas (clases abstractas).
Los descriptores de acceso de un evento virtual heredado se pueden invalidar en una clase derivada mediante la
inclusión de una declaración de evento que especifique un override modificador. Esto se conoce como una
declaración de evento de reemplazo . Una declaración de evento de reemplazo no declara un nuevo evento.
En su lugar, simplemente especializa las implementaciones de los descriptores de acceso de un evento virtual
existente.
Una declaración de evento de reemplazo debe especificar exactamente los mismos modificadores de
accesibilidad, tipo y nombre que el evento invalidado.
Una declaración de evento de reemplazo puede incluir el sealed modificador. El uso de este modificador evita
que una clase derivada Reemplace el evento. Los descriptores de acceso de un evento sellado también están
sellados.
Es un error en tiempo de compilación que una declaración de evento de reemplazo incluya un new modificador.
A excepción de las diferencias en la sintaxis de declaración e invocación, los descriptores de acceso virtual,
sellado, invalidación y Abstract se comportan exactamente igual que los métodos virtuales, sellados, de
invalidación y abstractos. En concreto, las reglas descritas en métodos virtuales, métodos de invalidación,
métodos selladosy métodos abstractos se aplican como si los descriptores de acceso fueran métodos de un
formulario correspondiente. Cada descriptor de acceso corresponde a un método con un parámetro de valor
único del tipo de evento, un void tipo de valor devuelto y los mismos modificadores que el evento que lo
contiene.
Indizadores
*Indexer _ es un miembro que permite indizar un objeto de la misma manera que una matriz. Los indexadores
se declaran mediante _indexer_declaration * s:
indexer_declaration
: attributes? indexer_modifier* indexer_declarator indexer_body
;
indexer_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'virtual'
| 'sealed'
| 'override'
| 'abstract'
| 'extern'
| indexer_modifier_unsafe
;
indexer_declarator
: type 'this' '[' formal_parameter_list ']'
| type interface_type '.' 'this' '[' formal_parameter_list ']'
;
indexer_body
: '{' accessor_declarations '}'
| '=>' expression ';'
;
Un indexer_declaration puede incluir un conjunto de atributos (atributos) y una combinación válida de los
cuatro modificadores de acceso (modificadores de acceso), los new Modificadores (los nuevos modificadores),
virtual (métodos virtuales), (métodos de override invalidación), sealed (métodos sellados), abstract
(métodos abstractos) y extern (métodos externos).
Las declaraciones de indexador están sujetas a las mismas reglas que las declaraciones de método (métodos)
con respecto a las combinaciones válidas de modificadores, con la única excepción de que el modificador static
no se permite en una declaración de indexador.
Los modificadores virtual , override y abstract se excluyen mutuamente, excepto en un caso. Los abstract
override modificadores y se pueden usar juntos para que un indexador abstracto pueda reemplazar a uno
virtual.
El tipo de una declaración de indexador especifica el tipo de elemento del indizador introducido por la
declaración. A menos que el indizador sea una implementación explícita de un miembro de interfaz, el tipo va
seguido de la palabra clave this . Para una implementación explícita de un miembro de interfaz, el tipo va
seguido de un interface_type, un " . " y la palabra clave this . A diferencia de otros miembros, los indizadores
no tienen nombres definidos por el usuario.
En el formal_parameter_list se especifican los parámetros del indexador. La lista de parámetros formales de un
indizador corresponde a la de un método (parámetros de método), salvo que se debe especificar al menos un
parámetro y ref que out no se permiten los modificadores de parámetro y.
El tipo de un indexador y cada uno de los tipos a los que se hace referencia en el formal_parameter_list deben
ser al menos tan accesibles como el propio indizador (restricciones de accesibilidad).
Un indexer_body puede consistir en un "*cuerpo del descriptor de acceso***" o un _cuerpo de expresión*. En el
cuerpo de un descriptor de acceso, _accessor_declarations *, que debe incluirse en los { tokens "" y "" } ,
declare los descriptores de acceso (descriptores de acceso) de la propiedad. Los descriptores de acceso
especifican las instrucciones ejecutables asociadas a la lectura y la escritura de la propiedad.
Un cuerpo de expresión que consta de " => " seguido de una expresión E y un punto y coma es exactamente
equivalente al cuerpo de la instrucción { get { return E; } } y, por tanto, solo se puede usar para especificar
indizadores de solo captador en los que una sola expresión proporciona el resultado del captador.
Aunque la sintaxis para tener acceso a un elemento de indexador es la misma que para un elemento de matriz,
un elemento de indexador no se clasifica como una variable. Por lo tanto, no es posible pasar un elemento
indexador como ref argumento o out .
La lista de parámetros formales de un indexador define la firma (firmas y sobrecarga) del indexador. En
concreto, la firma de un indexador consta del número y los tipos de sus parámetros formales. El tipo de
elemento y los nombres de los parámetros formales no forman parte de la firma de un indexador.
La firma de un indizador debe ser diferente de las firmas de todos los demás indexadores declarados en la
misma clase.
Los indexadores y las propiedades son muy similares en concepto, pero difieren de las siguientes maneras:
Una propiedad se identifica por su nombre, mientras que un indexador se identifica mediante su firma.
Se tiene acceso a una propiedad a través de un simple_name (nombres simples) o un member_access
(acceso a miembros), mientras que se tiene acceso a un elemento de indexador a través de un
element_access (acceso a indexador).
Una propiedad puede ser un static miembro, mientras que un indexador siempre es un miembro de
instancia.
Un get descriptor de acceso de una propiedad corresponde a un método sin parámetros, mientras que un
get descriptor de acceso de un indizador corresponde a un método con la misma lista de parámetros
formales que el indexador.
Un set descriptor de acceso de una propiedad corresponde a un método con un único parámetro
denominado value , mientras que un set descriptor de acceso de un indizador corresponde a un método
con la misma lista de parámetros formales que el indexador, además de un parámetro adicional denominado
value .
Es un error en tiempo de compilación que un descriptor de acceso de indexador declare una variable local
con el mismo nombre que un parámetro de indizador.
En una declaración de propiedad de reemplazo, se tiene acceso a la propiedad heredada mediante la sintaxis
base.P , donde P es el nombre de la propiedad. En una declaración de indizador de reemplazo, se tiene
acceso al indizador heredado mediante la sintaxis base[E] , donde E es una lista de expresiones separadas
por comas.
No hay ningún concepto de "indexador implementado automáticamente". Es un error tener un indexador no
externo no abstracto con descriptores de acceso de punto y coma.
Aparte de estas diferencias, todas las reglas definidas en los descriptores de acceso y las propiedades
implementadas automáticamente se aplican a los descriptores de acceso del indizador y a los descriptores de
acceso de propiedades.
Cuando una declaración de indexador incluye un extern modificador, el indizador se dice que es un
*indexador externo . Dado que una declaración de indexador externa no proporciona ninguna implementación
real, cada una de sus _accessor_declarations * consta de un punto y coma.
En el ejemplo siguiente se declara una BitArray clase que implementa un indizador para tener acceso a los bits
individuales de la matriz de bits.
using System;
class BitArray
{
int[] bits;
int length;
Una instancia de la BitArray clase consume bastante menos memoria que un correspondiente bool[] (puesto
que cada valor de la antigua solo ocupa un bit en lugar de un byte), pero permite las mismas operaciones que
bool[] .
La CountPrimes clase siguiente usa un BitArray y el algoritmo clásico "criba" para calcular el número de
primos entre 1 y un máximo determinado:
class CountPrimes
{
static int Count(int max) {
BitArray flags = new BitArray(max + 1);
int count = 1;
for (int i = 2; i <= max; i++) {
if (!flags[i]) {
for (int j = i * 2; j <= max; j += i) flags[j] = true;
count++;
}
}
return count;
}
Tenga en cuenta que la sintaxis para tener acceso a los elementos de BitArray es exactamente igual que para
bool[] .
En el ejemplo siguiente se muestra una clase de cuadrícula de 26 * 10 que tiene un indizador con dos
parámetros. El primer parámetro debe ser una letra mayúscula o minúscula en el intervalo A-Z, y el segundo
debe ser un entero en el intervalo 0-9.
using System;
class Grid
{
const int NumRows = 26;
const int NumCols = 10;
set {
c = Char.ToUpper(c);
if (c < 'A' || c > 'Z') {
throw new ArgumentException();
}
if (col < 0 || col >= NumCols) {
throw new IndexOutOfRangeException();
}
cells[c - 'A', col] = value;
}
}
}
Operadores
Un *operador _ es un miembro que define el significado de un operador de expresión que se puede aplicar a
las instancias de la clase. Los operadores se declaran mediante _operator_declaration * s:
operator_declaration
: attributes? operator_modifier+ operator_declarator operator_body
;
operator_modifier
: 'public'
| 'static'
| 'extern'
| operator_modifier_unsafe
;
operator_declarator
: unary_operator_declarator
| binary_operator_declarator
| conversion_operator_declarator
;
unary_operator_declarator
: type 'operator' overloadable_unary_operator '(' type identifier ')'
;
overloadable_unary_operator
: '+' | '-' | '!' | '~' | '++' | '--' | 'true' | 'false'
;
binary_operator_declarator
: type 'operator' overloadable_binary_operator '(' type identifier ',' type identifier ')'
;
overloadable_binary_operator
: '+' | '-' | '*' | '/' | '%' | '&' | '|' | '^' | '<<'
| right_shift | '==' | '!=' | '>' | '<' | '>=' | '<='
;
conversion_operator_declarator
: 'implicit' 'operator' type '(' type identifier ')'
| 'explicit' 'operator' type '(' type identifier ')'
;
operator_body
: block
| '=>' expression ';'
| ';'
;
Hay tres categorías de Operadores sobrecargables: operadores unarios (operadores unarios), operadores
binarios (operadores binarios) y operadores de conversión (operadores de conversión).
La operator_body es un punto y coma, un cuerpo de instrucción * o un cuerpo de expresión. Un cuerpo de
instrucción consta de un _block *, que especifica las instrucciones que se ejecutarán cuando se invoque el
operador. El bloque debe cumplir las reglas de los métodos que devuelven valores descritos en el cuerpo del
método. Un cuerpo de expresión consta de => seguido de una expresión y un punto y coma, y denota una
expresión única que se debe realizar cuando se invoca el operador.
En extern el caso de los operadores, el operator_body consta simplemente de un punto y coma. En el caso de
todos los demás operadores, el operator_body es un cuerpo de bloque o un cuerpo de expresión.
Las siguientes reglas se aplican a todas las declaraciones de operador:
Una declaración de operador debe incluir tanto un public static modificador como un modificador.
Los parámetros de un operador deben ser parámetros de valor (parámetros devalor). Se trata de un error en
tiempo de compilación para que una declaración de operador especifique ref out parámetros o.
La firma de un operador (operadores unarios, operadores binariosy operadores de conversión) debe ser
diferente de las firmas de todos los demás operadores declarados en la misma clase.
Todos los tipos a los que se hace referencia en una declaración de operador deben ser al menos tan
accesibles como el propio operador (restricciones de accesibilidad).
Es un error que el mismo modificador aparezca varias veces en una declaración de operador.
Cada categoría de operador impone restricciones adicionales, tal y como se describe en las secciones siguientes.
Al igual que otros miembros, las clases derivadas heredan los operadores declarados en una clase base. Dado
que las declaraciones de operador siempre requieren la clase o estructura en la que se declara que el operador
participa en la firma del operador, no es posible que un operador declarado en una clase derivada oculte un
operador declarado en una clase base. Por lo tanto, el new modificador nunca es necesario y, por lo tanto, nunca
se permite en una declaración de operador.
Puede encontrar información adicional sobre operadores unarios y binarios en operadores.
Puede encontrar información adicional sobre los operadores de conversión en conversiones definidas por el
usuario.
Operadores unarios
Las siguientes reglas se aplican a las declaraciones de operadores unarios, donde T denota el tipo de instancia
de la clase o el struct que contiene la declaración de operador:
Un operador unario + , - , ! o ~ debe tomar un parámetro único de tipo T o T? y puede devolver
cualquier tipo.
Un operador unario ++ or -- debe tomar un único parámetro de tipo T o T? y debe devolver el mismo
tipo o un tipo derivado de él.
Un operador unario true or false debe tomar un parámetro único de tipo T o T? y debe devolver el
tipo bool .
La firma de un operador unario está formada por el token del operador ( + , - , ! , ~ , ++ , -- , true o
false ) y el tipo del único parámetro formal. El tipo de valor devuelto no forma parte de la firma de un
operador unario, ni tampoco el nombre del parámetro formal.
Los true false operadores unarios y requieren la declaración de pares. Se produce un error en tiempo de
compilación si una clase declara uno de estos operadores sin declarar también el otro. Los true false
operadores y se describen con más detalle en operadores lógicos condicionales definidos por el usuario y en
Expresiones booleanas.
En el ejemplo siguiente se muestra una implementación y el uso posterior de operator ++ para una clase de
vector entero:
public class IntVector
{
public IntVector(int length) {...}
class Test
{
static void Main() {
IntVector iv1 = new IntVector(4); // vector of 4 x 0
IntVector iv2;
Observe cómo el método de operador devuelve el valor generado agregando 1 al operando, al igual que los
operadores de incremento y decremento de postfijo(operadores de incremento ydecremento postfijo) y los
operadores de incremento y decremento de prefijo (operadores de incremento y decremento deprefijo). A
diferencia de C++, este método no necesita modificar directamente el valor de su operando. De hecho, la
modificación del valor de operando infringiría la semántica estándar del operador de incremento de postfijo.
Operadores binarios
Las siguientes reglas se aplican a las declaraciones de operadores binarios, donde T denota el tipo de instancia
de la clase o el struct que contiene la declaración de operador:
Un operador binario sin desplazamiento debe tomar dos parámetros, al menos uno de los cuales debe tener
el tipo T o y T? puede devolver cualquier tipo.
Un << operador OR binario >> debe tomar dos parámetros, el primero debe tener el tipo T o T? y el
segundo de los cuales debe tener el tipo int o int? , y puede devolver cualquier tipo.
La firma de un operador binario consta del token de operador ( + , - , * , / , % , & , | , ^ , << , >> ,,,,,
== != > < >= o <= ) y los tipos de los dos parámetros formales. El tipo de valor devuelto y los nombres
de los parámetros formales no forman parte de la firma de un operador binario.
Ciertos operadores binarios requieren la declaración de pares. Para cada declaración de cualquiera de los
operadores de un par, debe haber una declaración coincidente del otro operador del par. Dos declaraciones de
operador coinciden cuando tienen el mismo tipo de valor devuelto y el mismo tipo para cada parámetro. Los
operadores siguientes requieren una declaración de pares:
operator == y operator !=
operator > y operator <
operator >= y operator <=
Operadores de conversión
Una declaración de operador de conversión introduce una conversión definida por el usuario (conversiones
definidas por el usuario) que aumenta las conversiones implícitas y explícitas predefinidas.
Una declaración de operador de conversión que incluye la implicit palabra clave presenta una conversión
implícita definida por el usuario. Las conversiones implícitas pueden producirse en diversas situaciones,
incluidas las invocaciones de miembros de función, las expresiones de conversión y las asignaciones. Esto se
describe con más detalle en conversiones implícitas.
Una declaración de operador de conversión que incluye la explicit palabra clave presenta una conversión
explícita definida por el usuario. Las conversiones explícitas pueden producirse en expresiones de conversión y
se describen con más detalle en conversiones explícitas.
Un operador de conversión convierte de un tipo de origen, indicado por el tipo de parámetro del operador de
conversión, en un tipo de destino, indicado por el tipo de valor devuelto del operador de conversión.
Para un tipo de origen S y tipo de destino determinados, T si S o T son tipos que aceptan valores NULL,
permiten S0 y T0 hacen referencia a sus tipos subyacentes, de lo contrario, S0 y T0 son iguales a S y T
respectivamente. Una clase o struct puede declarar una conversión de un tipo de origen S a un tipo de destino
T solo si se cumplen todas las condiciones siguientes:
En el caso de estas reglas, los parámetros de tipo asociados a S o T se consideran tipos únicos que no tienen
ninguna relación de herencia con otros tipos y se omiten las restricciones en esos parámetros de tipo.
En el ejemplo
las dos primeras declaraciones de operador se permiten porque, para los fines de los indizadores. 3, T y int y
string respectivamente se consideran tipos únicos sin relación. Sin embargo, el tercer operador es un error
porque C<T> es la clase base de D<T> .
En la segunda regla, se sigue que un operador de conversión debe convertir a o desde el tipo de clase o
estructura en el que se declara el operador. Por ejemplo, es posible que un tipo de clase o estructura C defina
una conversión de C a int y de int a C , pero no de int a bool .
No es posible volver a definir directamente una conversión predefinida. Por lo tanto, no se permite que los
operadores de conversión se conviertan de o a object porque ya existen conversiones implícitas y explícitas
entre object y todos los demás tipos. Del mismo modo, ni los tipos de origen ni de destino de una conversión
pueden ser un tipo base del otro, ya que una conversión ya existirá.
Sin embargo, es posible declarar operadores en tipos genéricos que, para argumentos de tipo concretos,
especifiquen conversiones que ya existan como conversiones predefinidas. En el ejemplo
struct Convertible<T>
{
public static implicit operator Convertible<T>(T value) {...}
public static explicit operator T(Convertible<T> value) {...}
}
Cuando object el tipo se especifica como un argumento de tipo para T , el segundo operador declara una
conversión que ya existe (una conversión implícita y, por tanto, también una conversión explícita de cualquier
tipo al tipo object ).
En los casos en los que existe una conversión predefinida entre dos tipos, se omiten las conversiones definidas
por el usuario entre esos tipos. Concretamente:
Si existe una conversión implícita predefinida (conversiones implícitas) del tipo S al tipo T , S se omiten
todas las conversiones definidas por el usuario (implícitas o explícitas) de a T .
Si existe una conversión explícita predefinida (conversiones explícitas) del tipo S al tipo T , S se omiten
las conversiones explícitas definidas por el usuario de a T . Asimismo,
Si T es un tipo de interfaz, S se omiten las conversiones implícitas definidas por el usuario de a T .
De lo contrario, las conversiones implícitas definidas por el usuario de S a T se siguen teniendo en cuenta.
En todos los tipos object , pero los operadores declarados por el Convertible<T> tipo anterior no entran en
conflicto con las conversiones predefinidas. Por ejemplo:
Sin embargo, para object el tipo, las conversiones predefinidas ocultan las conversiones definidas por el
usuario en todos los casos, pero una:
No se permiten conversiones definidas por el usuario para convertir de o a interface_type s. En concreto, esta
restricción garantiza que no se produzcan transformaciones definidas por el usuario al realizar la conversión a
un interface_type y que una conversión a una interface_type se realice correctamente solo si el objeto que se
está convirtiendo implementa realmente el interface_type especificado.
La firma de un operador de conversión está formada por el tipo de origen y el tipo de destino. (Tenga en cuenta
que esta es la única forma de miembro para la que participa el tipo de valor devuelto en la firma.) La implicit
explicit clasificación o de un operador de conversión no forma parte de la firma del operador. Por lo tanto,
una clase o struct no puede declarar un implicit explicit operador de conversión y con los mismos tipos de
origen y de destino.
En general, las conversiones implícitas definidas por el usuario deben diseñarse para que nunca se produzcan
excepciones y nunca se pierda información. Si una conversión definida por el usuario puede dar lugar a
excepciones (por ejemplo, porque el argumento de origen está fuera del intervalo) o la pérdida de información
(como descartar los bits de orden superior), esa conversión debe definirse como una conversión explícita.
En el ejemplo
using System;
la conversión de Digit a byte es implícita porque nunca produce excepciones o pierde información, pero la
conversión de byte a Digit es explícita, ya que Digit solo puede representar un subconjunto de los valores
posibles de byte .
Constructores de instancias
Un *constructor de instancia _ es un miembro que implementa las acciones necesarias para inicializar una
instancia de una clase. Los constructores de instancias se declaran mediante _constructor_declaration * s:
constructor_declaration
: attributes? constructor_modifier* constructor_declarator constructor_body
;
constructor_modifier
: 'public'
| 'protected'
| 'internal'
| 'private'
| 'extern'
| constructor_modifier_unsafe
;
constructor_declarator
: identifier '(' formal_parameter_list? ')' constructor_initializer?
;
constructor_initializer
: ':' 'base' '(' argument_list? ')'
| ':' 'this' '(' argument_list? ')'
;
constructor_body
: block
| ';'
;
Un constructor_declaration puede incluir un conjunto de atributos (atributos), una combinación válida de los
cuatro modificadores de acceso (modificadores de acceso) y un extern modificador (métodos externos). No se
permite que una declaración de constructor incluya el mismo modificador varias veces.
El identificador de un constructor_declarator debe nombrar la clase en la que se declara el constructor de
instancia. Si se especifica cualquier otro nombre, se produce un error en tiempo de compilación.
El formal_parameter_list opcional de un constructor de instancia está sujeto a las mismas reglas que la
formal_parameter_list de un método (métodos). La lista de parámetros formales define la firma (firmas y
sobrecarga) de un constructor de instancia y rige el proceso por el cual la resolución de sobrecarga (inferencia
de tipos) selecciona un constructor de instancia determinado en una invocación.
Cada uno de los tipos a los que se hace referencia en el formal_parameter_list de un constructor de instancia
debe ser al menos igual de accesible que el propio constructor (restricciones de accesibilidad).
El constructor_initializer opcional especifica otro constructor de instancia que se va a invocar antes de ejecutar
las instrucciones proporcionadas en el constructor_body de este constructor de instancia. Esto se describe con
más detalle en inicializadores de constructor.
Cuando una declaración de constructor incluye un extern modificador, se dice que el constructor es un
*constructor externo _. Dado que una declaración de constructor externo no proporciona ninguna
implementación real, su _constructor_body * consta de un punto y coma. En el caso de todos los demás
constructores, el constructor_body se compone de un bloque que especifica las instrucciones para inicializar una
nueva instancia de la clase. Esto corresponde exactamente al bloque de un método de instancia con un void
tipo de valor devuelto (cuerpo del método).
No se heredan los constructores de instancia. Por lo tanto, una clase no tiene ningún constructor de instancia
que no sea el declarado realmente en la clase. Si una clase no contiene ninguna declaración de constructor de
instancia, se proporciona automáticamente un constructor de instancia predeterminado (constructores
predeterminados).
Los constructores de instancias se invocan mediante object_creation_expression s (expresiones de creación de
objetos) y a través de constructor_initializer s.
Inicializadores del constructor
Todos los constructores de instancia (excepto los de la clase object ) incluyen implícitamente una invocación de
otro constructor de instancia inmediatamente antes de la constructor_body. El constructor para invocar
implícitamente está determinado por el constructor_initializer:
Un inicializador de constructor de instancia con el formato base(argument_list) o base() hace que se
invoque un constructor de instancia de la clase base directa. Ese constructor se selecciona utilizando
argument_list , si está presente y las reglas de resolución de sobrecarga de la resolución de sobrecarga. El
conjunto de constructores de instancias candidatas consta de todos los constructores de instancia accesibles
contenidos en la clase base directa, o el constructor predeterminado (constructores predeterminados), si no
se declara ningún constructor de instancia en la clase base directa. Si este conjunto está vacío, o si no se
puede identificar un único constructor de instancia mejor, se produce un error en tiempo de compilación.
Un inicializador de constructor de instancia con el formato this(argument-list) o this() hace que se
invoque un constructor de instancia de la propia clase. El constructor se selecciona utilizando argument_list ,
si está presente y las reglas de resolución de sobrecarga de la resolución de sobrecarga. El conjunto de
constructores de instancias candidatas se compone de todos los constructores de instancia accesibles
declarados en la propia clase. Si este conjunto está vacío, o si no se puede identificar un único constructor de
instancia mejor, se produce un error en tiempo de compilación. Si una declaración de constructor de
instancia incluye un inicializador de constructor que invoca al propio constructor, se produce un error en
tiempo de compilación.
Si un constructor de instancia no tiene ningún inicializador de constructor, base() se proporciona
implícitamente un inicializador de constructor con el formato. Por lo tanto, una declaración de constructor de
instancia con el formato
C(...) {...}
es exactamente equivalente a
class A
{
public A(int x, int y) {}
}
class B: A
{
public B(int x, int y): base(x + y, x - y) {}
}
Un inicializador de constructor de instancia no puede tener acceso a la instancia que se está creando. Por lo
tanto, es un error en tiempo de compilación hacer referencia a this una expresión de argumento del
inicializador de constructor, como es un error en tiempo de compilación para que una expresión de argumento
haga referencia a un miembro de instancia a través de un simple_name.
Inicializadores de variables de instancia
Cuando un constructor de instancia no tiene ningún inicializador de constructor o tiene un inicializador de
constructor con el formato base(...) , ese constructor realiza implícitamente las inicializaciones especificadas
por los variable_initializer s de los campos de instancia declarados en su clase. Esto corresponde a una secuencia
de asignaciones que se ejecutan inmediatamente después de la entrada al constructor y antes de la invocación
implícita del constructor de clase base directo. Los inicializadores de variable se ejecutan en el orden textual en
el que aparecen en la declaración de clase.
Ejecución del constructor
Los inicializadores de variables se transforman en instrucciones de asignación, y estas instrucciones de
asignación se ejecutan antes de la invocación del constructor de instancia de clase base. Este orden garantiza
que todos los campos de instancia se inicializan mediante sus inicializadores de variable antes de que se ejecute
cualquier instrucción que tenga acceso a esa instancia.
Dado el ejemplo
using System;
class A
{
public A() {
PrintFields();
}
class B: A
{
int x = 1;
int y;
public B() {
y = -1;
}
Cuando new B() se utiliza para crear una instancia de B , se genera el siguiente resultado:
x = 1, y = 0
class A
{
int x = 1, y = -1, count;
public A() {
count = 0;
}
public A(int n) {
count = n;
}
}
class B: A
{
double sqrt2 = Math.Sqrt(2.0);
ArrayList items = new ArrayList(100);
int max;
contiene varios inicializadores variables; también contiene inicializadores de constructor de ambos formularios (
base y this ). El ejemplo corresponde al código que se muestra a continuación, donde cada comentario indica
una instrucción insertada automáticamente (la sintaxis utilizada para las invocaciones de constructor insertadas
automáticamente no es válida, pero simplemente sirve para ilustrar el mecanismo).
using System.Collections;
class A
{
int x, y, count;
public A() {
x = 1; // Variable initializer
y = -1; // Variable initializer
object(); // Invoke object() constructor
count = 0;
}
public A(int n) {
x = 1; // Variable initializer
y = -1; // Variable initializer
object(); // Invoke object() constructor
count = n;
}
}
class B: A
{
double sqrt2;
ArrayList items;
int max;
Constructores predeterminados
Si una clase no contiene ninguna declaración de constructor de instancia, se proporciona automáticamente un
constructor de instancia predeterminado. Ese constructor predeterminado simplemente invoca el constructor
sin parámetros de la clase base directa. Si la clase es abstracta, se protege la accesibilidad declarada para el
constructor predeterminado. De lo contrario, la accesibilidad declarada para el constructor predeterminado es
Public. Por lo tanto, el constructor predeterminado siempre tiene el formato
or
class Message
{
object sender;
string text;
Constructores privados
Cuando una clase T declara solo constructores de instancia privados, no es posible que las clases fuera del
texto del programa de T deriven de T o creen directamente instancias de T . Por lo tanto, si una clase solo
contiene miembros estáticos y no está pensado para que se creen instancias, agregar un constructor de
instancia privado vacío impedirá la creación de instancias. Por ejemplo:
La Trig clase agrupa los métodos relacionados y las constantes, pero no está previsto que se creen instancias
de ellos. Por lo tanto, declara un único constructor de instancia privado vacío. Se debe declarar al menos un
constructor de instancia para suprimir la generación automática de un constructor predeterminado.
Parámetros de constructor de instancia opcionales
La this(...) forma de inicializador de constructor se utiliza normalmente junto con la sobrecarga para
implementar parámetros de constructor de instancia opcionales. En el ejemplo
class Text
{
public Text(): this(0, 0, null) {}
los dos primeros constructores de instancia simplemente proporcionan los valores predeterminados para los
argumentos que faltan. Ambos usan un this(...) inicializador de constructor para invocar el tercer constructor
de instancia, que realmente realiza el trabajo de inicializar la nueva instancia. El efecto es el de los parámetros de
constructor opcionales:
Constructores estáticos
Un *constructor estático _ es un miembro que implementa las acciones necesarias para inicializar un tipo de
clase cerrada. Los constructores estáticos se declaran mediante _static_constructor_declaration * s:
static_constructor_declaration
: attributes? static_constructor_modifiers identifier '(' ')' static_constructor_body
;
static_constructor_modifiers
: 'extern'? 'static'
| 'static' 'extern'?
| static_constructor_modifiers_unsafe
;
static_constructor_body
: block
| ';'
;
class Test
{
static void Main() {
A.F();
B.F();
}
}
class A
{
static A() {
Console.WriteLine("Init A");
}
public static void F() {
Console.WriteLine("A.F");
}
}
class B
{
static B() {
Console.WriteLine("Init B");
}
public static void F() {
Console.WriteLine("B.F");
}
}
Init A
A.F
Init B
B.F
Dado que la A llamada a desencadena la ejecución del constructor estático de A.F , y la B llamada a
desencadena la ejecución del constructor estático de B.F .
Es posible crear dependencias circulares que permitan observar los campos estáticos con inicializadores
variables en su estado de valor predeterminado.
En el ejemplo
using System;
class A
{
public static int X;
static A() {
X = B.Y + 1;
}
}
class B
{
public static int Y = A.X + 1;
static B() {}
genera el resultado
X = 1, Y = 2
Para ejecutar el Main método, el sistema primero ejecuta el inicializador para B.Y , antes B del constructor
estático de la clase. Y el inicializador de hace que A el constructor estático de se ejecute porque A.X se hace
referencia al valor de. El constructor estático de A a su vez continúa para calcular el valor de X y, al hacerlo,
recupera el valor predeterminado de Y , que es cero. A.X por tanto, se inicializa en 1. El proceso de ejecución
de los A inicializadores de campo estáticos y del constructor estático se completa y vuelve al cálculo del valor
inicial Y de, cuyo resultado es 2.
Dado que el constructor estático se ejecuta exactamente una vez para cada tipo de clase construido cerrado, es
un lugar cómodo para aplicar comprobaciones en tiempo de ejecución en el parámetro de tipo que no se puede
comprobar en tiempo de compilación a través de restricciones (restricciones de parámetros de tipo). Por
ejemplo, el tipo siguiente usa un constructor estático para exigir que el argumento de tipo sea una enumeración:
Destructores
Un *destructor _ es un miembro que implementa las acciones necesarias para destruir una instancia de una
clase. Un destructor se declara mediante una _destructor_declaration *:
destructor_declaration
: attributes? 'extern'? '~' identifier '(' ')' destructor_body
| destructor_declaration_unsafe
;
destructor_body
: block
| ';'
;
class A
{
~A() {
Console.WriteLine("A's destructor");
}
}
class B: A
{
~B() {
Console.WriteLine("B's destructor");
}
}
class Test
{
static void Main() {
B b = new B();
b = null;
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
is
B's destructor
A's destructor
Dado que se llama a los destructores en una cadena de herencia en orden, desde la más derivada hasta la
menos derivada.
Los destructores se implementan invalidando el método virtual Finalize en System.Object . Los programas de
C# no pueden invalidar este método ni llamarlo (o invalidarlo) directamente. Por ejemplo, el programa
class A
{
override protected void Finalize() {} // error
class A
{
void Finalize() {} // permitted
}
Iterators
Un miembro de función (miembros de función) implementado mediante un bloque de iteradores (bloques) se
denomina iterador .
Un bloque de iterador se puede usar como cuerpo de un miembro de función siempre que el tipo de valor
devuelto del miembro de función correspondiente sea una de las interfaces de enumerador (interfaces de
enumerador) o una de las interfaces enumerables (interfaces enumerables). Puede producirse como
method_body, operator_body o accessor_body, mientras que los eventos, constructores de instancias,
constructores estáticos y destructores no se pueden implementar como iteradores.
Cuando un miembro de función se implementa mediante un bloque de iteradores, se trata de un error en
tiempo de compilación para que la lista de parámetros formales del miembro de función especifique cualquier
ref out parámetro o.
Interfaces de enumerador
Las interfaces de enumerador son la interfaz no genérica System.Collections.IEnumerator y todas las
creaciones de instancias de la interfaz genérica System.Collections.Generic.IEnumerator<T> . Por motivos de
brevedad, en este capítulo se hace referencia a estas interfaces como IEnumerator y IEnumerator<T> ,
respectivamente.
Interfaces enumerables
Las interfaces enumerables son la interfaz no genérica System.Collections.IEnumerable y todas las
creaciones de instancias de la interfaz genérica System.Collections.Generic.IEnumerable<T> . Por motivos de
brevedad, en este capítulo se hace referencia a estas interfaces como IEnumerable y IEnumerable<T> ,
respectivamente.
Tipo yield
Un iterador genera una secuencia de valores, todo el mismo tipo. Este tipo se denomina tipo yield del iterador.
El tipo yield de un iterador que devuelve IEnumerator o IEnumerable es object .
El tipo yield de un iterador que devuelve IEnumerator<T> o IEnumerable<T> es T .
Objetos Enumerator
Cuando un miembro de función que devuelve un tipo de interfaz de enumerador se implementa mediante un
bloque de iteradores, al invocar al miembro de función no se ejecuta inmediatamente el código en el bloque de
iteradores. En su lugar, se crea y se devuelve un objeto de enumerador . Este objeto encapsula el código
especificado en el bloque de iteradores y la ejecución del código en el bloque de iterador se produce cuando se
invoca el método del objeto de enumerador MoveNext . Un objeto de enumerador tiene las siguientes
características:
Implementa IEnumerator y IEnumerator<T> , donde T es el tipo yield del iterador.
Implementa System.IDisposable .
Se inicializa con una copia de los valores de argumento (si existen) y el valor de instancia pasado al miembro
de función.
Tiene cuatro Estados posibles: *Before , Running, Suspended y After, y se encuentra inicialmente en el estado
_ *Before**.
Un objeto de enumerador suele ser una instancia de una clase de enumerador generada por el compilador que
encapsula el código en el bloque de iteradores e implementa las interfaces del enumerador, pero otros métodos
de implementación son posibles. Si el compilador genera una clase de enumerador, esa clase se anidará, directa
o indirectamente, en la clase que contiene el miembro de función, tendrá accesibilidad privada y tendrá un
nombre reservado para uso del compilador (identificadores).
Un objeto de enumerador puede implementar más interfaces de las especificadas anteriormente.
En las secciones siguientes se describe el comportamiento exacto de los MoveNext Current miembros, y
Dispose de IEnumerable las IEnumerable<T> implementaciones de la interfaz y que proporciona un objeto de
enumerador.
Tenga en cuenta que los objetos de enumerador no admiten el IEnumerator.Reset método. Al invocar este
método, System.NotSupportedException se produce una excepción.
El método MoveNext
El método de un objeto de enumerador encapsula el código de un bloque de iteradores. Al invocar el
MoveNext
MoveNext método, se ejecuta código en el bloque de iterador y se establece la Current propiedad del objeto de
enumerador según corresponda. La acción precisa realizada por MoveNext depende del estado del objeto de
enumerador cuando MoveNext se invoca:
Si el estado del objeto de enumerador es anterior , al invocar MoveNext :
Cambia el estado a en ejecución .
Inicializa los parámetros (incluido this ) del bloque de iteradores en los valores de argumento y el
valor de instancia guardados cuando se inicializó el objeto de enumerador.
Ejecuta el bloque de iterador desde el principio hasta que se interrumpe la ejecución (como se
describe a continuación).
Si el estado del objeto de enumerador se está ejecutando , no se especifica el resultado de la invocación
MoveNext .
Si el estado del objeto de enumerador es suspendido , al invocar MoveNext :
Cambia el estado a en ejecución .
Restaura los valores de todas las variables locales y los parámetros (incluido este) en los valores
guardados cuando la ejecución del bloque de iterador se suspendió por última vez. Tenga en cuenta
que el contenido de los objetos a los que hacen referencia estas variables puede haber cambiado
desde la llamada anterior a MoveNext.
Reanuda la ejecución del bloque de iterador inmediatamente después de la yield return instrucción
que provocó la suspensión de la ejecución y continúa hasta que se interrumpe la ejecución (como se
describe a continuación).
Si el estado del objeto de enumerador es después de, la llamada a MoveNext devuelve false .
Cuando MoveNext ejecuta el bloque de iterador, la ejecución se puede interrumpir de cuatro maneras: mediante
una yield return instrucción, mediante una yield break instrucción, al encontrar el final del bloque de
iterador y una excepción que se produce y se propaga fuera del bloque de iterador.
Cuando yield return se encuentra una instrucción (la instrucción yield):
La expresión dada en la instrucción se evalúa, se convierte implícitamente al tipo Yield y se asigna a la
Current propiedad del objeto enumerador.
Se suspende la ejecución del cuerpo del iterador. Se guardan los valores de todas las variables locales
y los parámetros (incluido this ), tal y como se encuentra en la ubicación de esta yield return
instrucción. Si la yield return instrucción está dentro de uno o más try bloques, los finally
bloques asociados no se ejecutan en este momento.
El estado del objeto de enumerador se cambia a Suspended .
El MoveNext método vuelve true a su llamador, lo que indica que la iteración se ha avanzado
correctamente al valor siguiente.
Cuando yield break se encuentra una instrucción (la instrucción yield):
Si la yield break instrucción está dentro de uno o más try bloques, finally se ejecutan los
bloques asociados.
El estado del objeto de enumerador se cambia a después de.
El MoveNext método vuelve false a su llamador, lo que indica que la iteración ha finalizado.
Cuando se encuentra el final del cuerpo del iterador:
El estado del objeto de enumerador se cambia a después de.
El MoveNext método vuelve false a su llamador, lo que indica que la iteración ha finalizado.
Cuando se produce una excepción y se propaga fuera del bloque de iteradores:
finally La propagación de excepciones habrá ejecutado los bloques adecuados en el cuerpo del
iterador.
El estado del objeto de enumerador se cambia a después de.
La propagación de excepciones continúa hasta el autor de la llamada del MoveNext método.
Propiedad actual
La propiedad de un objeto de enumerador Current se ve afectada por yield return las instrucciones del
bloque de iteradores.
Cuando un objeto de enumerador está en el estado *Suspended _, el valor de Current es el valor establecido
por la llamada anterior a MoveNext . Cuando un objeto de enumerador está en los Estados Before, Running o _
*After**, el resultado del acceso Current no se especifica.
En el caso de un iterador con un tipo de Yield distinto de object , el resultado del acceso Current a través de la
implementación del objeto de enumerador IEnumerable se corresponde con Current el acceso a través de la
implementación del objeto de enumerador IEnumerator<T> y la conversión del resultado a object .
El método Dispose
El Dispose método se usa para limpiar la iteración y el objeto de enumerador se coloca en el estado después .
Si el estado del objeto de enumerador es *Before _, al invocar se Dispose cambia el estado a _ *después de
* *.
Si el estado del objeto de enumerador se está ejecutando , no se especifica el resultado de la invocación
Dispose .
Si el estado del objeto de enumerador es suspendido , al invocar Dispose :
Cambia el estado a en ejecución .
Ejecuta cualquier bloque Finally como si la última instrucción ejecutada yield return fuera una
yield break instrucción. Si esto hace que se produzca una excepción y se propague fuera del cuerpo
del iterador, el estado del objeto de enumerador se establece en después de y la excepción se
propaga al llamador del Dispose método.
Cambia el estado a después de.
Si el estado del objeto de enumerador es After , la invocación de Dispose no tiene ningún efecto.
Objetos enumerables
Cuando un miembro de función que devuelve un tipo de interfaz enumerable se implementa mediante un
bloque de iteradores, al invocar al miembro de función no se ejecuta inmediatamente el código en el bloque de
iteradores. En su lugar, se crea y se devuelve un objeto enumerable . El método del objeto enumerable
GetEnumerator devuelve un objeto de enumerador que encapsula el código especificado en el bloque de
iterador y la ejecución del código en el bloque de iterador se produce cuando se invoca el método del objeto de
enumerador MoveNext . Un objeto enumerable tiene las siguientes características:
Implementa IEnumerable y IEnumerable<T> , donde T es el tipo yield del iterador.
Se inicializa con una copia de los valores de argumento (si existen) y el valor de instancia pasado al miembro
de función.
Un objeto enumerable es normalmente una instancia de una clase Enumerable generada por el compilador que
encapsula el código en el bloque de iteradores e implementa las interfaces enumerables, pero otros métodos de
implementación son posibles. Si el compilador genera una clase Enumerable, esa clase se anidará, directa o
indirectamente, en la clase que contiene el miembro de función, tendrá accesibilidad privada y tendrá un
nombre reservado para uso del compilador (identificadores).
Un objeto enumerable puede implementar más interfaces de las especificadas anteriormente. En concreto, un
objeto enumerable también puede implementar IEnumerator y IEnumerator<T> , lo que permite que sirva como
un enumerador y un enumerador. En ese tipo de implementación, la primera vez que se invoca el método de un
objeto enumerable GetEnumerator , se devuelve el propio objeto Enumerable. Las siguientes invocaciones de la
del objeto enumerable GetEnumerator , si las hay, devuelven una copia del objeto Enumerable. Por lo tanto, cada
enumerador devuelto tiene su propio estado y los cambios en un enumerador no afectarán a otro.
El método GetEnumerator
Un objeto enumerable proporciona una implementación de los GetEnumerator métodos de IEnumerable las
IEnumerable<T> interfaces y. Los dos GetEnumerator métodos comparten una implementación común que
adquiere y devuelve un objeto de enumerador disponible. El objeto de enumerador se inicializa con los valores
de argumento y el valor de instancia guardados cuando se inicializó el objeto enumerable, pero de lo contrario
el objeto de enumerador funciona como se describe en objetos de enumerador.
Ejemplo de implementación
En esta sección se describe una posible implementación de iteradores en términos de construcciones estándar
de C#. La implementación que se describe aquí se basa en los mismos principios utilizados por el compilador de
Microsoft C#, pero no es una implementación asignada o la única posible.
La Stack<T> clase siguiente implementa su GetEnumerator método mediante un iterador. El iterador enumera
los elementos de la pila en orden descendente.
using System;
using System.Collections;
using System.Collections.Generic;
public T Pop() {
T result = items[--count];
items[count] = default(T);
return result;
}
El GetEnumerator método se puede traducir en una instancia de una clase de enumerador generada por el
compilador que encapsula el código en el bloque de iteradores, como se muestra a continuación.
public T Current {
get { return __current; }
}
object IEnumerator.Current {
get { return __current; }
}
void IEnumerator.Reset() {
throw new NotSupportedException();
}
}
}
En la traducción anterior, el código del bloque de iterador se convierte en una máquina de Estados y se coloca
en el MoveNext método de la clase de enumerador. Además, la variable local i se convierte en un campo en el
objeto de enumerador para que pueda seguir existiendo en las invocaciones de MoveNext .
En el ejemplo siguiente se imprime una tabla de multiplicación simple de los enteros comprendidos entre 1 y
10. El FromTo método en el ejemplo devuelve un objeto enumerable y se implementa mediante un iterador.
using System;
using System.Collections.Generic;
class Test
{
static IEnumerable<int> FromTo(int from, int to) {
while (from <= to) yield return from++;
}
El FromTo método se puede traducir en una instancia de una clase Enumerable generada por el compilador que
encapsula el código en el bloque de iteradores, como se muestra a continuación.
using System;
using System.Threading;
using System.Collections;
using System.Collections.Generic;
class Test
{
...
class __Enumerable1:
IEnumerable<int>, IEnumerable,
IEnumerator<int>, IEnumerator
{
int __state;
int __current;
int __from;
int from;
int to;
int i;
IEnumerator IEnumerable.GetEnumerator() {
return (IEnumerator)GetEnumerator();
}
public int Current {
get { return __current; }
}
object IEnumerator.Current {
get { return __current; }
}
void IEnumerator.Reset() {
throw new NotSupportedException();
}
}
}
La clase Enumerable implementa las interfaces enumerables y las interfaces de enumerador, lo que permite que
sirva como un enumerador y un enumerador. La primera vez que GetEnumerator se invoca el método, se
devuelve el propio objeto Enumerable. Las siguientes invocaciones de la del objeto enumerable GetEnumerator ,
si las hay, devuelven una copia del objeto Enumerable. Por lo tanto, cada enumerador devuelto tiene su propio
estado y los cambios en un enumerador no afectarán a otro. El Interlocked.CompareExchange método se usa
para garantizar la operación segura para subprocesos.
Los from to parámetros y se convierten en campos en la clase Enumerable. Dado from que se modifica en el
bloque de iterador, __from se introduce un campo adicional para contener el valor inicial dado a from en cada
enumerador.
El MoveNext método produce una excepción InvalidOperationException si se llama cuando __state es 0 . Esto
protege contra el uso del objeto enumerable como un objeto de enumerador sin llamar primero a
GetEnumerator .
En el ejemplo siguiente se muestra una clase de árbol simple. La Tree<T> clase implementa su GetEnumerator
método mediante un iterador. El iterador enumera los elementos del árbol en orden infijo.
using System;
using System.Collections.Generic;
class Program
{
static Tree<T> MakeTree<T>(T[] items, int left, int right) {
if (left > right) return null;
int i = (left + right) / 2;
return new Tree<T>(items[i],
MakeTree(items, left, i - 1),
MakeTree(items, i + 1, right));
}
El GetEnumerator método se puede traducir en una instancia de una clase de enumerador generada por el
compilador que encapsula el código en el bloque de iteradores, como se muestra a continuación.
public T Current {
get { return __current; }
}
object IEnumerator.Current {
get { return __current; }
}
case 0:
__state = -1;
if (__this.left == null) goto __yield_value;
__left = __this.left.GetEnumerator();
goto case 1;
case 1:
__state = -2;
if (!__left.MoveNext()) goto __left_dispose;
__current = __left.Current;
__state = 1;
return true;
__left_dispose:
__state = -1;
__left.Dispose();
__yield_value:
__current = __this.value;
__state = 2;
return true;
case 2:
__state = -1;
if (__this.right == null) goto __end;
__right = __this.right.GetEnumerator();
goto case 3;
case 3:
__state = -3;
if (!__right.MoveNext()) goto __right_dispose;
__current = __right.Current;
__state = 3;
return true;
__right_dispose:
__state = -1;
__right.Dispose();
__end:
__state = 4;
break;
}
}
finally {
if (__state < 0) Dispose();
}
return false;
return false;
}
case 1:
case -2:
__left.Dispose();
break;
case 3:
case -3:
__right.Dispose();
break;
}
}
finally {
__state = 4;
}
}
void IEnumerator.Reset() {
throw new NotSupportedException();
}
}
}
El compilador generado objetos temporales utilizado en las foreach instrucciones se eleva en __left los
__right campos y del objeto de enumerador. El __state campo del objeto de enumerador se actualiza
cuidadosamente para que se Dispose() llame correctamente al método correcto si se produce una excepción.
Tenga en cuenta que no es posible escribir el código traducido con foreach instrucciones sencillas.
Funciones asincrónicas
Un método (métodos) o una función anónima (expresiones de función anónima) con el async modificador se
denomina *Async function _. En general, el término _ Async* se usa para describir cualquier tipo de función
que tenga el async modificador.
Es un error en tiempo de compilación para que la lista de parámetros formales de una función asincrónica
especifique cualquier ref out parámetro o.
El return_type de un método asincrónico debe ser void o un tipo de tarea . Los tipos de tarea son
System.Threading.Tasks.Task y los tipos construidos a partir de System.Threading.Tasks.Task<T> . Por motivos
de brevedad, en este capítulo se hace referencia a estos tipos como Task y Task<T> , respectivamente. Se dice
que un método asincrónico que devuelve un tipo de tarea es el que devuelve la tarea.
La definición exacta de los tipos de tarea es la implementación definida, pero desde el punto de vista del idioma,
un tipo de tarea se encuentra en uno de los Estados incompleto, correcto o erróneo. Una tarea con errores
registra una excepción pertinente. Una correcta Task<T> registra un resultado de tipo T . Los tipos de tarea
son de espera y, por tanto, pueden ser los operandos de las expresiones Await (expresiones Await).
Una invocación de función asincrónica tiene la capacidad de suspender la evaluación por medio de expresiones
Await (expresiones Await) en su cuerpo. La evaluación se puede reanudar más adelante en el punto de la
expresión Await de suspensión a través de un delegado * reanudación _. El delegado de reanudación es de
tipo System.Action y, cuando se invoca, la evaluación de la invocación de la función asincrónica se reanudará
desde la expresión Await en la que se quedó. La llamada al llamador actual* de una invocación de función
asincrónica es el autor de la llamada original si la invocación de la función nunca se ha suspendido o el autor de
la llamada más reciente del delegado de reanudación en caso contrario.
Evaluación de una función asincrónica que devuelve una tarea
La invocación de una función asincrónica que devuelve una tarea hace que se genere una instancia del tipo de
tarea devuelto. Esto se denomina la tarea devuelta de la función Async. La tarea está inicialmente en un estado
incompleto.
A continuación, se evalúa el cuerpo de la función asincrónica hasta que se suspenda (al llegar a una expresión
Await) o finalice, donde el control de punto se devuelve al autor de la llamada, junto con la tarea devuelta.
Cuando finaliza el cuerpo de la función asincrónica, la tarea devuelta se saca del estado incompleto:
Si el cuerpo de la función finaliza como resultado de alcanzar una instrucción return o el final del cuerpo, se
registra cualquier valor de resultado en la tarea devuelta, que se pone en un estado correcto.
Si el cuerpo de la función finaliza como resultado de una excepción no detectada (la instrucción throw), la
excepción se registra en la tarea devuelta que se coloca en un estado de error.
Evaluación de una función asincrónica que devuelve void
Si el tipo de valor devuelto de la función Async es void , la evaluación difiere de la anterior de la siguiente
manera: dado que no se devuelve ninguna tarea, la función comunica en su lugar la finalización y las
excepciones al contexto de sincronización del subproceso actual. La definición exacta del contexto de
sincronización depende de la implementación, pero es una representación de "Dónde" se está ejecutando el
subproceso actual. El contexto de sincronización se notifica cuando se inicia la evaluación de una función
asincrónica que devuelve void, se completa correctamente o provoca que se produzca una excepción no
detectada.
Esto permite que el contexto realice un seguimiento del número de funciones asincrónicas que devuelven void
que se están ejecutando en él y de decidir cómo propagar las excepciones que salen de ellas.
Estructuras
18/09/2021 • 33 minutes to read
Las estructuras son similares a las clases en que representan las estructuras de datos que pueden contener
miembros de datos y miembros de función. Sin embargo, a diferencia de las clases, las estructuras son tipos de
valor y no requieren la asignación del montón. Una variable de un tipo de estructura contiene directamente los
datos del struct, mientras que una variable de un tipo de clase contiene una referencia a los datos, la última
conocida como un objeto.
Los structs son particularmente útiles para estructuras de datos pequeñas que tengan semánticas de valor. Los
números complejos, los puntos de un sistema de coordenadas o los pares clave-valor de un diccionario son
buenos ejemplos de structs. La clave de estas estructuras de datos es que tienen pocos miembros de datos, que
no requieren el uso de la herencia o la identidad referencial, y que se pueden implementar de forma cómoda
mediante la semántica de valores donde la asignación copia el valor en lugar de la referencia.
Como se describe en tipos simples, los tipos simples proporcionados por C#, como int , double y bool , son
en realidad todos los tipos de estructura. Del mismo modo que estos tipos predefinidos son Structs, también es
posible usar estructuras y sobrecarga de operadores para implementar nuevos tipos "primitivos" en el lenguaje
C#. Al final de este capítulo (ejemplos de struct) se proporcionan dos ejemplos de estos tipos.
Declaraciones de estructuras
Una struct_declaration es una type_declaration (declaraciones de tipos) que declara un nuevo struct:
struct_declaration
: attributes? struct_modifier* 'partial'? 'struct' identifier type_parameter_list?
struct_interfaces? type_parameter_constraints_clause* struct_body ';'?
;
struct_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| struct_modifier_unsafe
;
Es un error en tiempo de compilación que el mismo modificador aparezca varias veces en una declaración de
estructura.
Los modificadores de una declaración de struct tienen el mismo significado que los de una declaración de clase
(declaraciones de clase).
Unmodifier (modificador)
El partial modificador indica que este struct_declaration es una declaración de tipos parciales. Varias
declaraciones de struct parciales con el mismo nombre dentro de una declaración de tipo o espacio de nombres
envolvente se combinan para formar una declaración de struct, siguiendo las reglas especificadas en tipos
parciales.
Interfaces de struct
Una declaración de estructura puede incluir una especificación de struct_interfaces , en cuyo caso se dice que el
struct implementa directamente los tipos de interfaz especificados.
struct_interfaces
: ':' interface_type_list
;
struct_body
: '{' struct_member_declaration* '}'
;
Miembros de estructuras
Los miembros de una estructura se componen de los miembros introducidos por su struct_member_declaration
s y los miembros heredados del tipo System.ValueType .
struct_member_declaration
: constant_declaration
| field_declaration
| method_declaration
| property_declaration
| event_declaration
| indexer_declaration
| operator_declaration
| constructor_declaration
| static_constructor_declaration
| type_declaration
| struct_member_declaration_unsafe
;
A excepción de las diferencias que se indican en diferencias entre clases y estructuras, las descripciones de los
miembros de clase proporcionados en miembros de clase a través de funciones asincrónicas también se aplican
a los miembros de estructura.
struct Node
{
int data;
Node next; // error, Node directly depends on itself
}
es un error porque Node contiene un campo de instancia de su propio tipo. Otro ejemplo
struct A { B b; }
struct B { C c; }
struct C { A a; }
fragmento de código
genera el valor 10 . La asignación de a para b crea una copia del valor y, b por tanto, no se ve afectada por
la asignación a a.x . Point En su lugar se ha declarado como una clase, el resultado sería 100 porque a y b
haría referencia al mismo objeto.
Herencia
Todos los tipos de struct se heredan implícitamente de la clase System.ValueType , que, a su vez, hereda de la
clase object . Una declaración de estructura puede especificar una lista de interfaces implementadas, pero no
es posible que una declaración de struct especifique una clase base.
Los tipos de struct nunca son abstractos y siempre están sellados implícitamente. abstract sealed Por lo
tanto, los modificadores y no se permiten en una declaración de estructura.
Puesto que no se admite la herencia para los Structs, la accesibilidad declarada de un miembro de struct no
puede ser protected ni protected internal .
Los miembros de función de un struct no pueden ser abstract o virtual , y el override modificador solo se
permite para invalidar métodos heredados de System.ValueType .
Asignación
La asignación a una variable de un tipo de struct crea una copia del valor que se va a asignar. Esto difiere de la
asignación a una variable de un tipo de clase, que copia la referencia pero no el objeto identificado por la
referencia.
De forma similar a una asignación, cuando se pasa un struct como parámetro de valor o se devuelve como
resultado de un miembro de función, se crea una copia de la estructura. Un struct se puede pasar por referencia
a un miembro de función mediante ref un out parámetro o.
Cuando una propiedad o un indizador de una estructura es el destino de una asignación, la expresión de
instancia asociada con el acceso a la propiedad o indizador debe estar clasificada como una variable. Si la
expresión de instancia se clasifica como un valor, se produce un error en tiempo de compilación. Esto se
describe con más detalle en asignación simple.
Valores predeterminados
Tal y como se describe en valores predeterminados, varios tipos de variables se inicializan automáticamente en
su valor predeterminado cuando se crean. En el caso de las variables de tipos de clase y otros tipos de
referencia, este valor predeterminado es null . Sin embargo, dado que los Structs son tipos de valor que no
pueden ser null , el valor predeterminado de un struct es el valor generado al establecer todos los campos de
tipo de valor en sus valores predeterminados y todos los campos de tipo de referencia en null .
En el ejemplo se hace referencia al Point struct declarado anteriormente.
Inicializa cada Point de la matriz con el valor generado al establecer los x campos y y en cero.
El valor predeterminado de un struct corresponde al valor devuelto por el constructor predeterminado de la
estructura (constructores predeterminados). A diferencia de una clase, un struct no puede declarar un
constructor de instancia sin parámetros. En su lugar, cada struct tiene implícitamente un constructor de instancia
sin parámetros que siempre devuelve el valor que se obtiene al establecer todos los campos de tipo de valor en
sus valores predeterminados y todos los campos de tipo de referencia en null .
Los Structs se deben diseñar para considerar el estado de inicialización predeterminado como un estado válido.
En el ejemplo
using System;
struct KeyValuePair
{
string key;
string value;
el constructor de instancia definido por el usuario protege solo los valores NULL cuando se llama
explícitamente. En los casos en KeyValuePair los que una variable está sujeta a la inicialización de valores
predeterminados, los key campos y serán value null y el struct debe estar preparado para controlar este
estado.
Conversiones boxing y unboxing
Un valor de un tipo de clase se puede convertir al tipo object o a un tipo de interfaz implementado por la clase
simplemente tratando la referencia como otro tipo en tiempo de compilación. Del mismo modo, un valor de tipo
object o un valor de un tipo de interfaz se puede volver a convertir a un tipo de clase sin cambiar la referencia
(pero, por supuesto, se requiere una comprobación de tipo en tiempo de ejecución en este caso).
Dado que los Structs no son tipos de referencia, estas operaciones se implementan de forma diferente para los
tipos de struct. Cuando un valor de un tipo de estructura se convierte al tipo object o a un tipo de interfaz
implementado por el struct, se produce una operación de conversión boxing. Del mismo modo, cuando un valor
de tipo object o un valor de un tipo de interfaz se vuelve a convertir a un tipo de estructura, se produce una
operación de conversión unboxing. Una diferencia clave de las mismas operaciones en los tipos de clase es que
la conversión boxing y la conversión unboxing copia el valor de la estructura dentro o fuera de la instancia de
conversión boxing. Por lo tanto, después de una operación de conversión boxing o unboxing, los cambios
realizados en la estructura desempaquetada no se reflejan en la estructura con conversión boxing.
Cuando un tipo de struct invalida un método virtual heredado de System.Object (como Equals , GetHashCode o
ToString ), la invocación del método virtual a través de una instancia del tipo struct no provoca la conversión
boxing. Esto es así incluso cuando el struct se utiliza como parámetro de tipo y la invocación se produce a través
de una instancia del tipo de parámetro de tipo. Por ejemplo:
using System;
struct Counter
{
int value;
class Program
{
static void Test<T>() where T: new() {
T x = new T();
Console.WriteLine(x.ToString());
Console.WriteLine(x.ToString());
Console.WriteLine(x.ToString());
}
1
2
3
Aunque el estilo no válido para ToString tiene efectos secundarios, en el ejemplo se muestra que no se ha
producido ninguna conversión boxing para las tres invocaciones de x.ToString() .
Del mismo modo, la conversión boxing nunca se produce implícitamente cuando se obtiene acceso a un
miembro en un parámetro de tipo restringido. Por ejemplo, supongamos ICounter que una interfaz contiene
un método Increment que se puede utilizar para modificar un valor. Si ICounter se utiliza como una restricción,
Increment se llama a la implementación del método con una referencia a la variable a la que Increment se
llamó, nunca a una copia con conversión boxing.
using System;
interface ICounter
{
void Increment();
}
void ICounter.Increment() {
value++;
}
}
class Program
{
static void Test<T>() where T: ICounter, new() {
T x = new T();
Console.WriteLine(x);
x.Increment(); // Modify x
Console.WriteLine(x);
((ICounter)x).Increment(); // Modify boxed copy of x
Console.WriteLine(x);
}
0
1
1
Para obtener más información sobre las conversiones boxing y unboxing, consulte Boxing y unboxing.
Significado de este
Dentro de un constructor de instancia o un miembro de función de instancia de una clase, this se clasifica
como un valor. Por lo tanto, aunque se this puede usar para hacer referencia a la instancia de a la que se
invocó el miembro de función, no es posible asignar a this en un miembro de función de una clase.
Dentro de un constructor de instancia de un struct, this corresponde a un out parámetro del tipo de
estructura y dentro de un miembro de función de instancia de un struct, this corresponde a un ref
parámetro del tipo de estructura. En ambos casos, this se clasifica como una variable y es posible modificar la
estructura completa para la que se invocó el miembro de la función mediante la asignación a this o pasando
esto como un ref out parámetro o.
Inicializadores de campo
Como se describe en valores predeterminados, el valor predeterminado de un struct consta del valor que se
obtiene al establecer todos los campos de tipo de valor en sus valores predeterminados y todos los campos de
tipo de referencia en null . Por esta razón, un struct no permite que las declaraciones de campo de instancia
incluyan inicializadores variables. Esta restricción solo se aplica a los campos de instancia. Los campos estáticos
de un struct pueden incluir inicializadores variables.
En el ejemplo
struct Point
{
public int x = 1; // Error, initializer not permitted
public int y = 1; // Error, initializer not permitted
}
struct Point
{
int x, y;
public int X {
set { x = value; }
}
public int Y {
set { y = value; }
}
No se puede llamar a ninguna función miembro de instancia (incluidos los descriptores de acceso set para las
propiedades X y Y ) hasta que todos los campos del struct que se está construyendo se hayan asignado
definitivamente. La única excepción implica las propiedades implementadas automáticamente (propiedades
implementadas automáticamente). Las reglas de asignación definitiva (expresiones de asignación simple)
excluyen específicamente la asignación a una propiedad automática de un tipo de estructura dentro de un
constructor de instancia de ese tipo de estructura: una asignación se considera una asignación definitiva del
campo oculto de respaldo de la propiedad automática. Por lo tanto, se permite lo siguiente:
struct Point
{
public int X { get; set; }
public int Y { get; set; }
Destructores
Un struct no puede declarar un destructor.
Constructores estáticos
Los constructores estáticos para Structs siguen la mayoría de las mismas reglas que para las clases. La ejecución
de un constructor estático para un tipo de struct lo desencadena el primero de los siguientes eventos para que
se produzca dentro de un dominio de aplicación:
Se hace referencia A un miembro estático del tipo struct.
Se llama a un constructor declarado explícitamente del tipo de estructura.
La creación de valores predeterminados (valores predeterminados) de tipos struct no desencadena el
constructor estático. (Un ejemplo de esto es el valor inicial de los elementos de una matriz).
Ejemplos de estructuras
A continuación se muestran dos ejemplos importantes del uso de struct tipos para crear tipos que se pueden
usar de forma similar a los tipos predefinidos del lenguaje, pero con semántica modificada.
Tipo entero de base de datos
El DBInt struct siguiente implementa un tipo entero que puede representar el conjunto completo de valores del
int tipo, además de un estado adicional que indica un valor desconocido. Un tipo con estas características se
utiliza normalmente en las bases de datos.
using System;
// When the defined field is true, this DBInt represents a known value
// which is stored in the value field. When the defined field is false,
// this DBInt represents an unknown value, and the value field is 0.
int value;
bool defined;
DBInt(int value) {
this.value = value;
this.defined = true;
}
using System;
sbyte value;
// Private instance constructor. The value parameter must be -1, 0, or 1.
DBBool(int value) {
this.value = (sbyte)value;
}
Una matriz es una estructura de datos que contiene un número de variables a las que se tiene acceso a través de
índices calculados. Las variables contenidas en una matriz, denominadas también elementos de la matriz, son
todas del mismo tipo y este tipo se conoce como tipo de elemento de la matriz.
Una matriz tiene un rango que determina el número de índices asociados a cada elemento de la matriz. El rango
de una matriz también se conoce como las dimensiones de la matriz. Una matriz con un rango de uno se
denomina *matriz unidimensional _. Una matriz con un rango mayor que uno se denomina matriz
multidimensional* *. Las matrices multidimensionales de tamaño específico se suelen denominar matrices
bidimensionales, matrices tridimensionales, etc.
Cada dimensión de una matriz tiene una longitud asociada que es un número entero mayor o igual que cero.
Las longitudes de las dimensiones no forman parte del tipo de la matriz, sino que se establecen cuando se crea
una instancia del tipo de matriz en tiempo de ejecución. La longitud de una dimensión determina el intervalo
válido de índices de esa dimensión: para una dimensión de longitud, los N índices pueden oscilar entre 0 y
N - 1 ambos inclusive. El número total de elementos de una matriz es el producto de las longitudes de cada
dimensión de la matriz. Si una o varias de las dimensiones de una matriz tienen una longitud de cero, se dice
que la matriz está vacía.
El tipo de elemento de una matriz puede ser cualquiera, incluido un tipo de matriz.
Tipos de matriz
Un tipo de matriz se escribe como un non_array_type seguido de uno o varios rank_specifier s:
array_type
: non_array_type rank_specifier+
;
non_array_type
: type
;
rank_specifier
: '[' dim_separator* ']'
;
dim_separator
: ','
;
En efecto, los rank_specifier s se leen de izquierda a derecha antes del tipo de elemento final que no es de
matriz. El tipo int[][,,][,] es una matriz unidimensional de matrices tridimensionales de matrices
bidimensionales de int .
En tiempo de ejecución, un valor de un tipo de matriz puede ser null o una referencia a una instancia de ese
tipo de matriz.
Tipo System. Array
El tipo System.Array es el tipo base abstracto de todos los tipos de matriz. Existe una conversión de referencia
implícita (conversiones de referencia implícita) de cualquier tipo de matriz a System.Array , y existe una
conversión de referencia explícita (conversiones de referencia explícita) de System.Array a cualquier tipo de
matriz. Tenga en cuenta que System.Array no es un array_type. En su lugar, es una class_type de la que se
derivan todos los array_type s.
En tiempo de ejecución, un valor de tipo System.Array puede ser null o una referencia a una instancia de
cualquier tipo de matriz.
Matrices y la interfaz IList genérica
Una matriz unidimensional T[] implementa la interfaz System.Collections.Generic.IList<T> ( IList<T> para
abreviar) y sus interfaces base. En consecuencia, hay una conversión implícita de T[] a IList<T> y sus
interfaces base. Además, si hay una conversión de referencia implícita de S a T , entonces S[] implementa
IList<T> y existe una conversión de referencia implícita de S[] a IList<T> y sus interfaces base
(conversiones de referencia implícitas). Si hay una conversión de referencia explícita de S a T , se produce una
conversión de referencia explícita de S[] a IList<T> y sus interfaces base (conversiones de referencia
explícitas). Por ejemplo:
using System.Collections.Generic;
class Test
{
static void Main() {
string[] sa = new string[5];
object[] oa1 = new object[5];
object[] oa2 = sa;
creación de matriz
Las instancias de matriz se crean mediante array_creation_expression(expresiones de creación de matrices) o
mediante declaraciones de variables locales o de campo que incluyen un array_initializer (inicializadores de
matriz).
Cuando se crea una instancia de matriz, se establecen el rango y la longitud de cada dimensión y, a
continuación, permanecen constantes para toda la duración de la instancia. En otras palabras, no es posible
cambiar el rango de una instancia de matriz existente, ni tampoco es posible cambiar el tamaño de sus
dimensiones.
Una instancia de matriz siempre es de un tipo de matriz. El System.Array tipo es un tipo Abstract en el que no se
pueden crear instancias.
Los elementos de las matrices creadas por array_creation_expression s siempre se inicializan en su valor
predeterminado (valores predeterminados).
Miembros de la matriz
Cada tipo de matriz hereda los miembros declarados por el System.Array tipo.
Covarianza de matrices
Para dos reference_type s A y B , si una conversión de referencia implícita (conversiones de referencia
implícita) o una conversión de referencia explícita (conversiones de referencia explícita) existen de A a B , la
misma conversión de referencia también existe desde el tipo de matriz A[R] al tipo de matriz B[R] , donde R
es cualquier rank_specifier determinado (pero lo mismo para ambos tipos de matriz). Esta relación se conoce
como covarianza de matriz . En particular, la covarianza de matrices significa que un valor de un tipo de matriz
A[R] puede ser realmente una referencia a una instancia de un tipo de matriz B[R] , siempre que exista una
conversión de referencia implícita de B a A .
Debido a la covarianza de matriz, las asignaciones a los elementos de las matrices de tipos de referencia
incluyen una comprobación en tiempo de ejecución que garantiza que el valor que se asigna al elemento de
matriz es realmente un tipo permitido (asignación simple). Por ejemplo:
class Test
{
static void Fill(object[] array, int index, int count, object value) {
for (int i = index; i < index + count; i++) array[i] = value;
}
Inicializadores de matriz
Los inicializadores de matriz se pueden especificar en declaraciones de campo (campos), declaraciones de
variables locales (declaraciones de variables locales) y expresiones de creación de matrices (expresiones de
creación de matrices):
array_initializer
: '{' variable_initializer_list? '}'
| '{' variable_initializer_list ',' '}'
;
variable_initializer_list
: variable_initializer (',' variable_initializer)*
;
variable_initializer
: expression
| array_initializer
;
Un inicializador de matriz consta de una secuencia de inicializadores de variables, que se incluyen entre " { " y
"" } tokens y separados por " , " tokens. Cada inicializador de variable es una expresión o, en el caso de una
matriz multidimensional, un inicializador de matriz anidada.
El contexto en el que se usa un inicializador de matriz determina el tipo de la matriz que se va a inicializar. En
una expresión de creación de matriz, el tipo de matriz precede inmediatamente al inicializador o se deduce de
las expresiones del inicializador de matriz. En una declaración de campo o variable, el tipo de matriz es el tipo
del campo o la variable que se declara. Cuando se usa un inicializador de matriz en una declaración de campo o
variable, como:
En el caso de una matriz unidimensional, el inicializador de matriz debe constar de una secuencia de expresiones
que son compatibles con la asignación con el tipo de elemento de la matriz. Las expresiones inicializan los
elementos de matriz en orden ascendente, empezando por el elemento en el índice cero. El número de
expresiones del inicializador de matriz determina la longitud de la instancia de la matriz que se va a crear. Por
ejemplo, el inicializador de matriz anterior crea una int[] instancia de la longitud 5 y, a continuación, inicializa
la instancia con los valores siguientes:
En el caso de una matriz multidimensional, el inicializador de matriz debe tener tantos niveles de anidamiento
como dimensiones haya en la matriz. El nivel de anidamiento más externo corresponde a la dimensión situada
más a la izquierda y el nivel de anidamiento más interno corresponde a la dimensión situada más a la derecha.
La longitud de cada dimensión de la matriz viene determinada por el número de elementos en el nivel de
anidamiento correspondiente en el inicializador de matriz. Para cada inicializador de matriz anidada, el número
de elementos debe ser el mismo que el de los demás inicializadores de matriz del mismo nivel. El ejemplo:
int[,] b = {{0, 1}, {2, 3}, {4, 5}, {6, 7}, {8, 9}};
crea una matriz bidimensional con una longitud de cinco para la dimensión situada más a la izquierda y una
longitud de dos para la dimensión situada más a la derecha:
b[0, 0] = 0; b[0, 1] = 1;
b[1, 0] = 2; b[1, 1] = 3;
b[2, 0] = 4; b[2, 1] = 5;
b[3, 0] = 6; b[3, 1] = 7;
b[4, 0] = 8; b[4, 1] = 9;
Si una dimensión distinta de la derecha se proporciona con una longitud de cero, se supone que las
dimensiones subsiguientes también tienen la longitud cero. El ejemplo:
int[,] c = {};
crea una matriz bidimensional con una longitud de cero para la dimensión situada más a la izquierda y la
derecha:
Cuando una expresión de creación de matriz incluye longitudes de dimensión explícitas y un inicializador de
matriz, las longitudes deben ser expresiones constantes y el número de elementos de cada nivel de anidamiento
debe coincidir con la longitud de dimensión correspondiente. Estos son algunos ejemplos:
int i = 3;
int[] x = new int[3] {0, 1, 2}; // OK
int[] y = new int[i] {0, 1, 2}; // Error, i not a constant
int[] z = new int[3] {0, 1, 2, 3}; // Error, length/initializer mismatch
Una interfaz define un contrato. Una clase o estructura que implementa una interfaz debe adherirse a su
contrato. Una interfaz puede heredar de varias interfaces base, y una clase o estructura puede implementar
varias interfaces.
Las interfaces pueden contener métodos, propiedades, eventos e indizadores. La propia interfaz no proporciona
implementaciones para los miembros que define. La interfaz simplemente especifica los miembros que deben
ser proporcionados por clases o Structs que implementan la interfaz.
Declaraciones de interfaz
Una interface_declaration es una type_declaration (declaraciones de tipos) que declara un nuevo tipo de interfaz.
interface_declaration
: attributes? interface_modifier* 'partial'? 'interface'
identifier variant_type_parameter_list? interface_base?
type_parameter_constraints_clause* interface_body ';'?
;
interface_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| interface_modifier_unsafe
;
Es un error en tiempo de compilación que el mismo modificador aparezca varias veces en una declaración de
interfaz.
El new modificador solo se permite en interfaces definidas dentro de una clase. Especifica que la interfaz oculta
un miembro heredado con el mismo nombre, tal y como se describe en el modificador New.
Los public protected internal modificadores,, y private controlan la accesibilidad de la interfaz.
Dependiendo del contexto en el que se produce la declaración de la interfaz, solo se pueden permitir algunos de
estos modificadores (sedeclarará la accesibilidad).
Unmodifier (modificador)
El partial modificador indica que este interface_declaration es una declaración de tipos parciales. Varias
declaraciones de interfaz parcial con el mismo nombre dentro de una declaración de tipo o espacio de nombres
envolvente se combinan para formar una declaración de interfaz, siguiendo las reglas especificadas en tipos
parciales.
Listas de parámetros de tipo variante
Las listas de parámetros de tipo variante solo se pueden producir en tipos de interfaz y delegado. La diferencia
de los type_parameter_list normales es el variance_annotation opcional en cada parámetro de tipo.
variant_type_parameter_list
: '<' variant_type_parameters '>'
;
variant_type_parameters
: attributes? variance_annotation? type_parameter
| variant_type_parameters ',' attributes? variance_annotation? type_parameter
;
variance_annotation
: 'in'
| 'out'
;
interface_base
: ':' interface_type_list
;
En el caso de un tipo de interfaz construido, las interfaces base explícitas se forman tomando las declaraciones
de la interfaz base explícita en la declaración de tipos genéricos y sustituyendo por cada type_parameter en la
declaración de la interfaz base, la type_argument correspondiente del tipo construido.
Las interfaces base explícitas de una interfaz deben ser al menos tan accesibles como la propia interfaz
(restricciones de accesibilidad). Por ejemplo, se trata de un error en tiempo de compilación para especificar una
private internal interfaz o en el interface_base de una public interfaz.
Se trata de un error en tiempo de compilación para una interfaz que hereda directa o indirectamente de sí
misma.
Las interfaces base de una interfaz son las interfaces base explícitas y sus interfaces base. En otras palabras, el
conjunto de interfaces base es el cierre transitivo completo de las interfaces base explícitas, sus interfaces base
explícitas, etc. Una interfaz hereda todos los miembros de sus interfaces base. En el ejemplo
interface IControl
{
void Paint();
}
interface_body
: '{' interface_member_declaration* '}'
;
Miembros de interfaz
Los miembros de una interfaz son los miembros heredados de las interfaces base y los miembros declarados
por la propia interfaz.
interface_member_declaration
: interface_method_declaration
| interface_property_declaration
| interface_event_declaration
| interface_indexer_declaration
;
Una declaración de interfaz puede declarar cero o más miembros. Los miembros de una interfaz deben ser
métodos, propiedades, eventos o indizadores. Una interfaz no puede contener constantes, campos, operadores,
constructores de instancias, destructores ni tipos ni una interfaz puede contener miembros estáticos de
cualquier tipo.
Todos los miembros de interfaz tienen acceso público de forma implícita. Se trata de un error en tiempo de
compilación para que las declaraciones de miembros de interfaz incluyan modificadores. En concreto, los
miembros de las interfaces no se pueden declarar con los modificadores abstract ,, public protected ,
internal , private , virtual , override o static .
En el ejemplo
public delegate void StringListEvent(IStringList sender);
declara una interfaz que contiene uno de los posibles tipos de miembros: un método, una propiedad, un evento
y un indexador.
Un interface_declaration crea un nuevo espacio de declaración (declaraciones) y el
interface_member_declaration s que contiene inmediatamente el interface_declaration introduce nuevos
miembros en este espacio de declaración. Las siguientes reglas se aplican a interface_member_declaration s:
El nombre de un método debe ser distinto de los nombres de todas las propiedades y eventos declarados en
la misma interfaz. Además, la firma (firmas y sobrecarga) de un método debe ser diferente de las firmas de
todos los demás métodos declarados en la misma interfaz, y dos métodos declarados en la misma interfaz
no pueden tener firmas que solo difieran en ref y out .
El nombre de una propiedad o evento debe ser diferente de los nombres de todos los demás miembros
declarados en la misma interfaz.
La firma de un indexador debe ser diferente de las firmas de los demás indexadores declarados en la misma
interfaz.
Los miembros heredados de una interfaz no son específicamente parte del espacio de declaración de la interfaz.
Por lo tanto, una interfaz puede declarar un miembro con el mismo nombre o signatura que un miembro
heredado. Cuando esto ocurre, se dice que el miembro de interfaz derivado oculta el miembro de interfaz base.
Ocultar un miembro heredado no se considera un error, pero hace que el compilador emita una advertencia.
Para suprimir la advertencia, la declaración del miembro de interfaz derivado debe incluir un new modificador
para indicar que el miembro derivado está pensado para ocultar el miembro base. Este tema se describe con
más detalle en ocultarse a travésde la herencia.
Si new se incluye un modificador en una declaración que no oculta un miembro heredado, se emite una
advertencia para ese efecto. Esta advertencia se suprime quitando el new modificador.
Tenga en cuenta que los miembros de la clase object no son, estrictamente hablando, miembros de cualquier
interfaz (miembros de lainterfaz). Sin embargo, los miembros de la clase object están disponibles a través de
la búsqueda de miembros en cualquier tipo de interfaz (búsqueda de miembros).
Métodos de interfaz
Los métodos de interfaz se declaran mediante interface_method_declaration s:
interface_method_declaration
: attributes? 'new'? return_type identifier type_parameter_list
'(' formal_parameter_list? ')' type_parameter_constraints_clause* ';'
;
no es válido porque el uso de T como una restricción de parámetro de tipo en U no es seguro para la entrada.
Si esta restricción no estuviera en su lugar, sería posible infringir la seguridad de tipos de la siguiente manera:
class B {}
class D : B{}
class E : B {}
class C : I<D> { public void M<U>() {...} }
...
I<B> b = new C();
b.M<E>();
En realidad, se trata de una llamada a C.M<E> . Pero esa llamada requiere que E derive de D , por lo que la
seguridad de tipos se infringiría aquí.
Propiedades de la interfaz
Las propiedades de interfaz se declaran mediante interface_property_declaration s:
interface_property_declaration
: attributes? 'new'? type identifier '{' interface_accessors '}'
;
interface_accessors
: attributes? 'get' ';'
| attributes? 'set' ';'
| attributes? 'get' ';' attributes? 'set' ';'
| attributes? 'set' ';' attributes? 'get' ';'
;
Los atributos, el tipo y el identificador de una declaración de propiedad de interfaz tienen el mismo significado
que los de una declaración de propiedad en una clase (propiedades).
Los descriptores de acceso de una declaración de propiedad de interfaz corresponden a los descriptores de
acceso de una declaración de propiedad de clase (descriptores deacceso), salvo que el cuerpo del descriptor de
acceso siempre debe ser un punto y coma Por lo tanto, los descriptores de acceso simplemente indican si la
propiedad es de lectura y escritura, de solo lectura o de solo escritura.
El tipo de una propiedad de interfaz debe ser seguro para la salida si hay un descriptor de acceso get y debe ser
seguro para la entrada si hay un descriptor de acceso set.
Eventos de interfaz
Los eventos de interfaz se declaran mediante interface_event_declaration s:
interface_event_declaration
: attributes? 'new'? 'event' type identifier ';'
;
Los atributos, el tipo y el identificador de una declaración de evento de interfaz tienen el mismo significado que
los de una declaración de evento en una clase (eventos).
El tipo de un evento de interfaz debe ser seguro para la entrada.
Indexadores de interfaz
Los indizadores de interfaz se declaran mediante interface_indexer_declaration s:
interface_indexer_declaration
: attributes? 'new'? type 'this' '[' formal_parameter_list ']' '{' interface_accessors '}'
;
Los atributos, el tipo y el formal_parameter_list de una declaración de indexador de interfaz tienen el mismo
significado que los de una declaración de indexador en una clase (indizadores).
Los descriptores de acceso de una declaración de indexador de interfaz corresponden a los descriptores de
acceso de una declaración de indizador de clase (indizadores), salvo que el cuerpo del descriptor de acceso
siempre debe ser un punto y coma. Por lo tanto, los descriptores de acceso indican simplemente si el indizador
es de lectura y escritura, de solo lectura o de solo escritura.
Todos los tipos de parámetros formales de un indizador de interfaz deben ser seguros para la entrada. Además,
cualquier out tipo de ref parámetro formal o también debe ser seguro para la salida. Tenga en cuenta que
incluso out se requiere que los parámetros sean seguros para la entrada, debido a una limitación de la
plataforma de ejecución subyacente.
El tipo de un indizador de interfaz debe ser seguro para la salida si hay un descriptor de acceso get y debe ser
seguro para la entrada si hay un descriptor de acceso set.
Acceso a miembros de interfaz
Se tiene acceso a los miembros de interfaz a través del acceso a miembros (acceso amiembros) y las
expresiones de acceso a indizador (acceso a indizador) del formulario I.M y I[A] , donde I es un tipo de
interfaz, M es un método, propiedad o evento de ese tipo de interfaz, y A es una lista de argumentos de
indizador.
Para las interfaces que son estrictamente herencia única (cada interfaz de la cadena de herencia tiene
exactamente cero o una interfaz base directa), los efectos de las reglas de búsqueda de miembros (búsqueda de
miembros), invocación de métodos (llamadas a métodos) e indexador (acceso a indexador) son exactamente los
mismos que para las clases y los Structs: los miembros más derivados ocultan los miembros menos derivados
con el mismo nombre o signatura. Sin embargo, en el caso de las interfaces de herencia múltiple, pueden
producirse ambigüedades cuando dos o más interfaces base no relacionadas declaran miembros con el mismo
nombre o signatura. En esta sección se muestran varios ejemplos de esas situaciones. En todos los casos, se
pueden usar conversiones explícitas para resolver las ambigüedades.
En el ejemplo
interface IList
{
int Count { get; set; }
}
interface ICounter
{
void Count(int i);
}
class C
{
void Test(IListCounter x) {
x.Count(1); // Error
x.Count = 1; // Error
((IList)x).Count = 1; // Ok, invokes IList.Count.set
((ICounter)x).Count(1); // Ok, invokes ICounter.Count
}
}
las dos primeras instrucciones producen errores en tiempo de compilación porque la búsqueda de miembros
(búsqueda de miembros) de Count en IListCounter es ambigua. Como se muestra en el ejemplo, la
ambigüedad se resuelve mediante la conversión x al tipo de interfaz base adecuado. Tales conversiones no
tienen ningún costo en tiempo de ejecución, simplemente consisten en ver la instancia como un tipo menos
derivado en tiempo de compilación.
En el ejemplo
interface IInteger
{
void Add(int i);
}
interface IDouble
{
void Add(double d);
}
class C
{
void Test(INumber n) {
n.Add(1); // Invokes IInteger.Add
n.Add(1.0); // Only IDouble.Add is applicable
((IInteger)n).Add(1); // Only IInteger.Add is a candidate
((IDouble)n).Add(1); // Only IDouble.Add is a candidate
}
}
class A
{
void Test(IDerived d) {
d.F(1); // Invokes ILeft.F
((IBase)d).F(1); // Invokes IBase.F
((ILeft)d).F(1); // Invokes ILeft.F
((IRight)d).F(1); // Invokes IBase.F
}
}
el miembro IBase.F está oculto por el ILeft.F miembro. La invocación d.F(1) selecciona ILeft.F , aunque
IBase.F parece que no está oculto en la ruta de acceso que conduce IRight .
La regla intuitiva para ocultar en interfaces de herencia múltiple es simplemente esto: Si un miembro está oculto
en una ruta de acceso, se oculta en todas las rutas de acceso. Dado que la ruta de acceso de IDerived a ILeft
IBase oculta IBase.F , el miembro también se oculta en la ruta de acceso de IDerived a a IRight IBase .
interface IControl
{
void Paint();
}
Implementaciones de interfaces
Las interfaces pueden ser implementadas por clases y Structs. Para indicar que una clase o estructura
implementa directamente una interfaz, el identificador de interfaz se incluye en la lista de clases base de la clase
o estructura. Por ejemplo:
interface ICloneable
{
object Clone();
}
interface IComparable
{
int CompareTo(object other);
}
Una clase o estructura que implementa directamente una interfaz también implementa directamente todas las
interfaces base de la interfaz de forma implícita. Esto es así incluso si la clase o estructura no enumera
explícitamente todas las interfaces base de la lista de clases base. Por ejemplo:
interface IControl
{
void Paint();
}
interface I1<V> {}
Las interfaces base de una declaración de clase genérica deben cumplir la regla de unicidad descrita en
singularidad de las interfaces implementadas.
Implementaciones de miembros de interfaz explícitos
A efectos de la implementación de interfaces, una clase o struct puede declarar implementaciones de
miembros de interfaz explícita . Una implementación explícita de un miembro de interfaz es un método, una
propiedad, un evento o una declaración de indexador que hace referencia a un nombre de miembro de interfaz
completo. Por ejemplo
interface IList<T>
{
T[] GetElements();
}
interface IDictionary<K,V>
{
V this[K key];
void Add(K key, V value);
}
No es posible tener acceso a una implementación explícita de un miembro de interfaz a través de su nombre
completo en una invocación de método, acceso a propiedades o acceso a indexador. Solo se puede tener acceso
a una implementación de miembro de interfaz explícita a través de una instancia de interfaz y, en ese caso, se
hace referencia a ella simplemente por su nombre de miembro.
Se trata de un error en tiempo de compilación para que una implementación explícita de un miembro de
interfaz incluya modificadores de acceso, y es un error en tiempo de compilación incluir los modificadores
abstract ,, virtual override o static .
Las implementaciones explícitas de miembros de interfaz tienen distintas características de accesibilidad que
otros miembros. Dado que nunca se puede obtener acceso a las implementaciones de miembros de interfaz
explícitos mediante su nombre completo en una invocación de método o un acceso de propiedad, se encuentran
en un sentido privado. Sin embargo, puesto que se puede tener acceso a ellos a través de una instancia de la
interfaz, también son públicos.
Las implementaciones explícitas de miembros de interfaz tienen dos propósitos principales:
Dado que no se puede acceder a las implementaciones de miembros de interfaz explícitos a través de
instancias de clase o struct, permiten excluir las implementaciones de interfaz de la interfaz pública de una
clase o struct. Esto es especialmente útil cuando una clase o estructura implementa una interfaz interna que
no es de interés para un consumidor de esa clase o estructura.
Las implementaciones explícitas de miembros de interfaz permiten la desambiguación de los miembros de
interfaz con la misma firma. Sin implementaciones explícitas de miembros de interfaz, sería imposible que
una clase o struct tenga implementaciones diferentes de miembros de interfaz con la misma signatura y el
mismo tipo de valor devuelto, lo que sería imposible para que una clase o struct tenga cualquier
implementación en todos los miembros de interfaz con la misma firma pero con distintos tipos de valor
devueltos.
Para que una implementación de miembro de interfaz explícita sea válida, la clase o estructura debe nombrar
una interfaz en su lista de clases base que contenga un miembro cuyo nombre completo, tipo y tipos de
parámetro coincidan exactamente con los de la implementación explícita del miembro de interfaz. Por lo tanto,
en la clase siguiente
interface IControl
{
void Paint();
}
la implementación explícita del miembro de interfaz de Paint debe escribirse como IControl.Paint .
Unicidad de interfaces implementadas
Las interfaces implementadas por una declaración de tipo genérico deben ser únicas para todos los tipos
construidos posibles. Sin esta regla, sería imposible determinar el método correcto al que llamar para
determinados tipos construidos. Por ejemplo, supongamos que se permitía escribir una declaración de clase
genérica como sigue:
interface I<T>
{
void F();
}
interface I<T>
{
void F();
}
invoca el método en Derived , puesto que se Derived<int,int> vuelve a implementar de forma eficaz I<int>
(reimplementación de la interfaz).
Implementación de métodos genéricos
Cuando un método genérico implementa implícitamente un método de interfaz, las restricciones
proporcionadas para cada parámetro de tipo de método deben ser equivalentes en ambas declaraciones
(después de que los parámetros de tipo de interfaz se reemplacen con los argumentos de tipo adecuados),
donde los parámetros de tipo de método se identifican por posiciones ordinales, de izquierda a derecha.
Sin embargo, cuando un método genérico implementa explícitamente un método de interfaz, no se permite
ninguna restricción en el método de implementación. En su lugar, las restricciones se heredan del método de
interfaz.
interface I<A,B,C>
{
void F<T>(T t) where T: A;
void G<T>(T t) where T: B;
void H<T>(T t) where T: C;
}
class C: I<object,C,string>
{
public void F<T>(T t) {...} // Ok
public void G<T>(T t) where T: C {...} // Ok
public void H<T>(T t) where T: string {...} // Error
}
class C: I<object,C,string>
{
...
void I<object,C,string>.H<T>(T t) {
string s = t; // Ok
H<T>(t);
}
}
En este ejemplo, la implementación de miembro de interfaz explícita invoca un método público que tiene
restricciones estrictamente más débiles. Tenga en cuenta que la asignación de t a s es válida, ya que T
hereda una restricción de T:string , aunque esta restricción no se pueda expresar en el código fuente.
Asignación de interfaz
Una clase o estructura debe proporcionar implementaciones de todos los miembros de las interfaces que se
enumeran en la lista de clases base de la clase o estructura. El proceso de buscar implementaciones de
miembros de interfaz en una clase o struct de implementación se conoce como asignación de interfaz .
La asignación de interfaz para una clase o struct C localiza una implementación para cada miembro de cada
interfaz especificada en la lista de clases base de C . La implementación de un miembro de interfaz
determinado I.M , donde I es la interfaz en la que M se declara el miembro, se determina examinando cada
clase o struct S , empezando por C y repitiendo cada clase base sucesiva de C , hasta que se encuentre una
coincidencia:
Si Scontiene una declaración de una implementación explícita de un miembro de interfaz que coincide con
I y M , este miembro es la implementación de I.M .
De lo contrario, si S contiene una declaración de un miembro público no estático que coincide con M , este
miembro es la implementación de I.M . Si hay más de un miembro que coincide, no se especifica qué
miembro es la implementación de I.M . Esta situación solo puede producirse si S es un tipo construido en
el que los dos miembros declarados en el tipo genérico tienen firmas diferentes, pero los argumentos de tipo
hacen que sus firmas sean idénticas.
Se produce un error en tiempo de compilación si no se pueden encontrar implementaciones para todos los
miembros de todas las interfaces especificadas en la lista de clases base de C . Tenga en cuenta que los
miembros de una interfaz incluyen a los miembros heredados de las interfaces base.
En lo que respecta a la asignación de interfaz, un miembro de clase A coincide con un miembro de interfaz B
cuando:
A y B son métodos, y el nombre, el tipo y las listas de parámetros formales de A y B son idénticos.
A y B son propiedades, el nombre y el tipo de A y B son idénticos, y A tienen los mismos descriptores
de acceso que B ( A se permite tener descriptores de acceso adicionales si no es una implementación de
miembro de interfaz explícita).
A y B son eventos, y el nombre y el tipo de A y B son idénticos.
A y B son indizadores, las listas de parámetros de tipo y formales de A y B son idénticas, y A tienen los
mismos descriptores de acceso que B ( A se le permite tener descriptores de acceso adicionales si no es
una implementación de miembro de interfaz explícita).
Las implicaciones importantes del algoritmo de asignación de interfaz son:
Las implementaciones de miembros de interfaz explícitos tienen prioridad sobre otros miembros de la
misma clase o estructura al determinar el miembro de clase o de estructura que implementa un miembro de
interfaz.
Ninguno de los miembros no públicos ni estáticos participan en la asignación de interfaz.
En el ejemplo
interface ICloneable
{
object Clone();
}
class C: ICloneable
{
object ICloneable.Clone() {...}
public object Clone() {...}
}
interface IForm
{
void Paint();
}
En este caso, los Paint métodos de IControl y IForm se asignan al Paint método en Page . También es
posible tener implementaciones de miembros de interfaz explícitas independientes para los dos métodos.
Si una clase o estructura implementa una interfaz que contiene miembros ocultos, algunos miembros deben
implementarse necesariamente mediante implementaciones explícitas de miembros de interfaz. Por ejemplo
interface IBase
{
int P { get; }
}
Una implementación de esta interfaz requeriría al menos una implementación explícita de un miembro de
interfaz y adoptaría uno de los siguientes formatos
class C: IDerived
{
int IBase.P { get {...} }
int IDerived.P() {...}
}
class C: IDerived
{
public int P { get {...} }
int IDerived.P() {...}
}
class C: IDerived
{
int IBase.P { get {...} }
public int P() {...}
}
Cuando una clase implementa varias interfaces que tienen la misma interfaz base, solo puede haber una
implementación de la interfaz base. En el ejemplo
interface IControl
{
void Paint();
}
no es posible tener implementaciones independientes para el IControl denominado en la lista de clases base,
IControl heredado por ITextBox y IControl heredado por IListBox . En realidad, no hay ninguna noción de
una identidad independiente para estas interfaces. En su lugar, las implementaciones de ITextBox y IListBox
comparten la misma implementación de IControl y ComboBox simplemente se considera que implementa tres
interfaces,, IControl ITextBox y IListBox .
Los miembros de una clase base participan en la asignación de interfaz. En el ejemplo
interface Interface1
{
void F();
}
class Class1
{
public void F() {}
public void G() {}
}
el Paint método en TextBox oculta el Paint método en Control , pero no modifica la asignación de
Control.Paint en IControl.Paint , y las llamadas a a través de Paint instancias de clase e instancias de
interfaz tendrán los siguientes efectos:
Sin embargo, cuando un método de interfaz se asigna a un método virtual en una clase, es posible que las
clases derivadas invaliden el método virtual y modifiquen la implementación de la interfaz. Por ejemplo, volver a
escribir las declaraciones anteriores en
interface IControl
{
void Paint();
}
Dado que las implementaciones explícitas de los miembros de interfaz no se pueden declarar como virtuales, no
es posible invalidar una implementación explícita de un miembro de interfaz. Sin embargo, es absolutamente
válido que una implementación de miembro de interfaz explícita llame a otro método, y ese otro método se
puede declarar como virtual para permitir que las clases derivadas lo invaliden. Por ejemplo
interface IControl
{
void Paint();
}
Aquí, las clases derivadas de Control pueden especializar la implementación de IControl.Paint invalidando el
PaintControl método.
interface IControl
{
void Paint();
}
interface IBase
{
void F();
}
class C: IDerived
{
void IBase.F() {...}
void IDerived.G() {...}
}
class D: C, IDerived
{
public void F() {...}
public void G() {...}
}
En este caso, la reimplementación de IDerived también vuelve a implementar IBase , y se asigna a IBase.F
D.F .
Aquí, la implementación de IMethods Maps F y G en métodos abstractos, que se debe invalidar en clases no
abstractas que se derivan de C .
Tenga en cuenta que las implementaciones explícitas de los miembros de interfaz no pueden ser abstractas, pero
las implementaciones de miembros de interfaz explícitas son de curso y permiten llamar a métodos abstractos.
Por ejemplo
interface IMethods
{
void F();
void G();
}
Aquí, las clases no abstractas que derivan de C deberían reemplazar FF y GG , lo que proporciona la
implementación real de IMethods .
Enumeraciones
18/09/2021 • 12 minutes to read
Un tipo de enumeración es un tipo de valor distinto (tipos de valor) que declara un conjunto de constantes
con nombre.
En el ejemplo
enum Color
{
Red,
Green,
Blue
}
declara un tipo de enumeración denominado Color con miembros Red , Green y Blue .
Declaraciones de enumeración
Una declaración de enumeración declara un nuevo tipo de enumeración. Una declaración de enumeración
comienza con la palabra clave enum y define el nombre, la accesibilidad, el tipo subyacente y los miembros de la
enumeración.
enum_declaration
: attributes? enum_modifier* 'enum' identifier enum_base? enum_body ';'?
;
enum_base
: ':' integral_type
;
enum_body
: '{' enum_member_declarations? '}'
| '{' enum_member_declarations ',' '}'
;
Cada tipo de enumeración tiene un tipo entero correspondiente denominado el tipo subyacente del tipo de
enumeración. Este tipo subyacente debe ser capaz de representar todos los valores de enumerador definidos en
la enumeración. Una declaración de enumeración puede declarar explícitamente un tipo subyacente de byte ,,
sbyte short , ushort , int , uint long o ulong . Tenga en cuenta que char no se puede utilizar como tipo
subyacente. Una declaración de enumeración que no declara explícitamente un tipo subyacente tiene un tipo
subyacente de int .
En el ejemplo
declara una enumeración con un tipo subyacente de long . Un programador puede optar por usar un tipo
subyacente de long , como en el ejemplo, para habilitar el uso de valores que se encuentran en el intervalo de
long , pero no en el intervalo de int , o para conservar esta opción en el futuro.
Modificadores de enumeración
Un enum_declaration puede incluir opcionalmente una secuencia de modificadores de enumeración:
enum_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
;
Es un error en tiempo de compilación que el mismo modificador aparezca varias veces en una declaración de
enumeración.
Los modificadores de una declaración de enumeración tienen el mismo significado que los de una declaración
de clase (modificadores de clase). Sin embargo, tenga en cuenta que los abstract sealed modificadores y no
están permitidos en una declaración de enumeración. Las enumeraciones no pueden ser abstractas y no
permiten la derivación.
Miembros de enumeración
El cuerpo de una declaración de tipo enum define cero o más miembros de enumeración, que son las constantes
con nombre del tipo enum. Dos miembros de enumeración no pueden tener el mismo nombre.
enum_member_declarations
: enum_member_declaration (',' enum_member_declaration)*
;
enum_member_declaration
: attributes? identifier ('=' constant_expression)?
;
Cada miembro de la enumeración tiene un valor constante asociado. El tipo de este valor es el tipo subyacente
de la enumeración contenedora. El valor constante para cada miembro de la enumeración debe estar en el
intervalo del tipo subyacente de la enumeración. En el ejemplo
Max = Blue
}
muestra una enumeración en la que dos miembros de enumeración ( Blue y Max ) tienen el mismo valor
asociado.
El valor asociado de un miembro de enumeración se asigna de forma implícita o explícita. Si la declaración del
miembro de enumeración tiene un inicializador constant_expression , el valor de esa expresión constante,
convertido implícitamente en el tipo subyacente de la enumeración, es el valor asociado del miembro de
enumeración. Si la declaración del miembro de enumeración no tiene inicializador, su valor asociado se
establece implícitamente, como se indica a continuación:
Si el miembro de la enumeración es el primer miembro de la enumeración declarado en el tipo de
enumeración, su valor asociado es cero.
De lo contrario, el valor asociado del miembro de enumeración se obtiene aumentando el valor asociado del
miembro de enumeración anterior textualmente en uno. Este aumento de valor debe estar dentro del
intervalo de valores que se puede representar mediante el tipo subyacente; de lo contrario, se producirá un
error en tiempo de compilación.
En el ejemplo
using System;
enum Color
{
Red,
Green = 10,
Blue
}
class Test
{
static void Main() {
Console.WriteLine(StringFromColor(Color.Red));
Console.WriteLine(StringFromColor(Color.Green));
Console.WriteLine(StringFromColor(Color.Blue));
}
case Color.Green:
return String.Format("Green = {0}", (int) c);
case Color.Blue:
return String.Format("Blue = {0}", (int) c);
default:
return "Invalid color";
}
}
}
imprime los nombres de miembro de enumeración y sus valores asociados. La salida es la siguiente:
Red = 0
Green = 10
Blue = 11
enum Circular
{
A = B,
B
}
produce un error en tiempo de compilación porque las declaraciones de A y B son circulares. A depende de
B explícitamente y B depende de A implícitamente.
Los miembros de enumeración tienen un nombre y tienen el ámbito de una manera exactamente análoga a los
campos de las clases. El ámbito de un miembro de enumeración es el cuerpo de su tipo de enumeración
contenedor. Dentro de ese ámbito, se puede hacer referencia a los miembros de enumeración por su nombre
simple. Desde el resto del código, el nombre de un miembro de enumeración debe estar calificado con el
nombre de su tipo de enumeración. Los miembros de enumeración no tienen ninguna accesibilidad declarada:
se puede acceder a un miembro de enumeración si se puede tener acceso a su tipo de enumeración contenedor.
Tipo System.Enum
El tipo System.Enum es la clase base abstracta de todos los tipos de enumeración (es distinto y diferente del tipo
subyacente del tipo de enumeración) y los miembros heredados de System.Enum están disponibles en cualquier
tipo de enumeración. Existe una conversión boxing (conversiones boxing) de cualquier tipo de enumeración a y
System.Enum existe una conversión unboxing (conversiones unboxing) de System.Enum a cualquier tipo de
enumeración.
Tenga en cuenta que System.Enum no es un enum_type. En su lugar, es una class_type de la que se derivan todos
los enum_type s. El tipo System.Enum hereda del tipo System.ValueType (el tipo System. ValueType), que, a su
vez, hereda del tipo object . En tiempo de ejecución, un valor de tipo System.Enum puede ser null o una
referencia a un valor de conversión boxing de cualquier tipo de enumeración.
Cada tipo de enumeración se deriva automáticamente de la clase System.Enum (que, a su vez, se deriva de
System.ValueType y object ). Por lo tanto, los métodos y las propiedades heredados de esta clase se pueden
utilizar en los valores de un tipo de enumeración.
Delegados
18/09/2021 • 19 minutes to read
Los delegados habilitan escenarios en los que otros lenguajes, como C++, Pascal y modula, se han direccionado
con punteros de función. A diferencia de los punteros de función de C++, sin embargo, los delegados están
totalmente orientados a objetos y, a diferencia de los punteros de C++ a las funciones miembro, los delegados
encapsulan una instancia de objeto y un método.
Una declaración de delegado define una clase que se deriva de la clase System.Delegate . Una instancia de
delegado encapsula una lista de invocación, que es una lista de uno o varios métodos, a la que se hace
referencia como una entidad a la que se puede llamar. En el caso de los métodos de instancia, una entidad a la
que se puede llamar está formada por una instancia y un método en esa instancia. En el caso de los métodos
estáticos, una entidad a la que se puede llamar consta simplemente de un método. Al invocar una instancia de
delegado con un conjunto de argumentos adecuado, se invoca a cada una de las entidades a las que se puede
llamar del delegado con el conjunto de argumentos especificado.
Una propiedad interesante y útil de una instancia de delegado es que no sabe ni le interesan las clases de los
métodos que encapsula; lo único que importa es que esos métodos sean compatibles (declaraciones de
delegado) con el tipo de delegado. Esto hace que los delegados sean perfectamente adecuados para la
invocación "anónima".
Declaraciones de delegado
Un delegate_declaration es un type_declaration (declaraciones de tipos) que declara un nuevo tipo de delegado.
delegate_declaration
: attributes? delegate_modifier* 'delegate' return_type
identifier variant_type_parameter_list?
'(' formal_parameter_list? ')' type_parameter_constraints_clause* ';'
;
delegate_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| delegate_modifier_unsafe
;
Es un error en tiempo de compilación que el mismo modificador aparezca varias veces en una declaración de
delegado.
El new modificador solo se permite en los delegados declarados dentro de otro tipo, en cuyo caso especifica
que dicho delegado oculte un miembro heredado con el mismo nombre, tal y como se describe en el
modificador New.
Los public protected internal modificadores,, y private controlan la accesibilidad del tipo de delegado. En
función del contexto en el que se produce la declaración de delegado, es posible que algunos de estos
modificadores no estén permitidos (sedeclare accesibilidad).
El nombre de tipo del delegado es Identifier.
El formal_parameter_list opcional especifica los parámetros del delegado y return_type indica el tipo de valor
devuelto del delegado.
El variant_type_parameter_list opcional (listas de parámetros de tipo variante) especifica los parámetros de tipo
para el delegado.
El tipo de valor devuelto de un tipo de delegado debe ser o tener seguridad void de salida (seguridad de
lavarianza).
Todos los tipos de parámetros formales de un tipo de delegado deben ser seguros para la entrada. Además, los
out ref tipos de parámetro o también deben ser de seguridad de salida. Tenga en cuenta que incluso out se
requiere que los parámetros sean seguros para la entrada, debido a una limitación de la plataforma de ejecución
subyacente.
Los tipos de delegado en C# son equivalentes de nombre, no estructuralmente equivalentes. En concreto, dos
tipos de delegado diferentes que tienen las mismas listas de parámetros y tipo de valor devuelto se consideran
tipos de delegado distintos. Sin embargo, las instancias de dos tipos de delegado distintos, pero
estructuralmente equivalentes, pueden compararse como iguales (operadores de igualdad de delegado).
Por ejemplo:
class A
{
public static int M1(int a, double b) {...}
}
class B
{
delegate int D2(int c, double d);
public static int M1(int f, double g) {...}
public static void M2(int k, double l) {...}
public static int M3(int g) {...}
public static void M4(int g) {...}
}
Los métodos A.M1 y B.M1 son compatibles con los tipos de delegado D1 y D2 , ya que tienen el mismo tipo
de valor devuelto y la misma lista de parámetros; sin embargo, estos tipos de delegado son dos tipos diferentes,
por lo que no son intercambiables. Los métodos B.M2 , B.M3 y B.M4 son incompatibles con los tipos de
delegado D1 y D2 , ya que tienen tipos de valor devuelto o listas de parámetros diferentes.
Al igual que otras declaraciones de tipos genéricos, se deben proporcionar argumentos de tipo para crear un
tipo de delegado construido. Los tipos de parámetro y el tipo de valor devuelto de un tipo de delegado
construido se crean sustituyendo, por cada parámetro de tipo de la declaración de delegado, el argumento de
tipo correspondiente del tipo de delegado construido. El tipo de valor devuelto y los tipos de parámetro
resultantes se usan para determinar qué métodos son compatibles con un tipo de delegado construido. Por
ejemplo:
class X
{
static bool F(int i) {...}
static bool G(string s) {...}
}
El método X.F es compatible con el tipo de delegado Predicate<int> y el método X.G es compatible con el
tipo de delegado Predicate<string> .
La única manera de declarar un tipo de delegado es a través de un delegate_declaration. Un tipo de delegado es
un tipo de clase que se deriva de System.Delegate . Los tipos de delegado son implícitamente sealed , por lo
que no se permite derivar ningún tipo de un tipo de delegado. Tampoco se permite derivar un tipo de clase no
delegado de System.Delegate . Tenga en cuenta que System.Delegate no es un tipo de delegado, sino que es un
tipo de clase del que se derivan todos los tipos de delegado.
C# proporciona una sintaxis especial para la invocación y creación de instancias de delegado. A excepción de la
creación de instancias, cualquier operación que se puede aplicar a una instancia de clase o clase también se
puede aplicar a una clase o instancia de delegado, respectivamente. En concreto, es posible tener acceso a los
miembros del System.Delegate tipo a través de la sintaxis de acceso a miembros habitual.
El conjunto de métodos encapsulado por una instancia de delegado se denomina lista de invocación. Cuando se
crea una instancia de delegado (compatibilidad con delegación) desde un único método, encapsula ese método
y su lista de invocación contiene solo una entrada. Sin embargo, cuando se combinan dos instancias de
delegado que no son NULL, sus listas de invocación se concatenan, en el operando izquierdo de orden y
operando derecho, para formar una nueva lista de invocación, que contiene dos o más entradas.
Los delegados se combinan mediante el operador binario + (operador de suma) y los += operadores
(asignación compuesta). Un delegado se puede quitar de una combinación de delegados, mediante -
eloperadorbinario (operador de resta) y los -= operadores (asignación compuesta). Los delegados se pueden
comparar para comprobar si son iguales (operadores de igualdad de delegado).
En el ejemplo siguiente se muestra la creación de instancias de varios delegados y sus listas de invocación
correspondientes:
class C
{
public static void M1(int i) {...}
public static void M2(int i) {...}
class Test
{
static void Main() {
D cd1 = new D(C.M1); // M1
D cd2 = new D(C.M2); // M2
D cd3 = cd1 + cd2; // M1 + M2
D cd4 = cd3 + cd1; // M1 + M2 + M1
D cd5 = cd4 + cd3; // M1 + M2 + M1 + M1 + M2
}
Cuando cd1 cd2 se crean instancias de y, cada una de ellas encapsula un método. Cuando cd3 se crea una
instancia de, tiene una lista de invocaciones de dos métodos, M1 y M2 , en ese orden. cd4 la lista de invocación
de contiene M1 , M2 y M1 , en ese orden. Por último, cd5 la lista de invocación de contiene M1 , M2 ,, M1
M1 y M2 , en ese orden. Para obtener más ejemplos de cómo combinar (y quitar) delegados, vea invocación de
delegado.
Compatibilidad de delegado
Un método o un delegado M es compatible con un tipo D de delegado si se cumplen todas las condiciones
siguientes:
D y M tienen el mismo número de parámetros, y cada parámetro de D tiene los mismos ref out
modificadores o que el parámetro correspondiente de M .
Para cada parámetro de valor (un parámetro sin ref out modificador o), existe una conversión de
identidad (conversión de identidad) o una conversión de referencia implícita (conversiones de referencia
implícita) del tipo de parámetro en D el tipo de parámetro correspondiente en M .
Para cada ref out parámetro o, el tipo de parámetro de D es el mismo que el tipo de parámetro en M .
Existe una conversión de referencia implícita o de identidad del tipo de valor devuelto de M al tipo de valor
devuelto de D .
class C
{
public static void M1(int i) {...}
public void M2(int i) {...}
}
class Test
{
static void Main() {
D cd1 = new D(C.M1); // static method
C t = new C();
D cd2 = new D(t.M2); // instance method
D cd3 = new D(cd2); // another delegate
}
}
Una vez creada la instancia, las instancias de delegado siempre hacen referencia al mismo objeto y método de
destino. Recuerde que, cuando se combinan dos delegados o uno se quita de otro, se produce un nuevo
delegado que tiene su propia lista de invocación. las listas de invocaciones de los delegados combinados o
quitados permanecen sin cambios.
Invocación de delegado
C# proporciona una sintaxis especial para invocar un delegado. Cuando se invoca una instancia de delegado que
no es null cuya lista de invocación contiene una entrada, invoca el método con los mismos argumentos en los
que se ha dado y devuelve el mismo valor que el método al que se hace referencia. (Vea invocaciones de
delegado para obtener información detallada sobre la invocación de delegado). Si se produce una excepción
durante la invocación de este tipo de delegado y esa excepción no se detecta dentro del método que se invocó,
la búsqueda de una cláusula catch de excepción continúa en el método que llamó al delegado, como si ese
método hubiera llamado directamente al método al que el delegado hizo referencia.
La invocación de una instancia de delegado cuya lista de invocaciones contiene varias entradas continúa
invocando cada uno de los métodos de la lista de invocación, de forma sincrónica, en orden. Cada método al
que se llama se pasa el mismo conjunto de argumentos que se asignó a la instancia del delegado. Si dicha
invocación de delegado incluye parámetros de referencia (parámetros de referencia), cada invocación de
método se producirá con una referencia a la misma variable. los cambios en esa variable por un método en la
lista de invocación serán visibles para los métodos más abajo en la lista de invocación. Si la invocación del
delegado incluye parámetros de salida o un valor devuelto, su valor final proviene de la invocación del último
delegado en la lista.
Si se produce una excepción durante el procesamiento de la invocación de este delegado, y esa excepción no se
detecta dentro del método que se invocó, la búsqueda de una cláusula catch de excepción continúa en el
método que llamó al delegado, y no se invocan los métodos que se encuentran más abajo en la lista de
invocación.
Al intentar invocar una instancia de delegado cuyo valor es null, se produce una excepción de tipo
System.NullReferenceException .
En el ejemplo siguiente se muestra cómo crear instancias, combinar, quitar e invocar delegados:
using System;
class C
{
public static void M1(int i) {
Console.WriteLine("C.M1: " + i);
}
class Test
{
static void Main() {
D cd1 = new D(C.M1);
cd1(-1); // call M1
cd3 += cd1;
cd3(20); // call M1, M2, then M1
C c = new C();
D cd4 = new D(c.M3);
cd3 += cd4;
cd3(30); // call M1, M2, M1, then M3
cd3 -= cd4;
cd3(50); // call M1 then M2
cd3 -= cd2;
cd3(60); // call M1
Como se muestra en la instrucción cd3 += cd1; , un delegado puede estar presente varias veces en una lista de
invocación. En este caso, simplemente se invoca una vez por cada repetición. En una lista de invocación como
esta, cuando se quita ese delegado, la última aparición en la lista de invocación es la que realmente se quita.
Inmediatamente antes de la ejecución de la instrucción final, cd3 -= cd1; , el delegado cd3 hace referencia a
una lista de invocación vacía. El intento de quitar un delegado de una lista vacía (o de quitar un delegado que no
existe de una lista no vacía) no es un error.
La salida generada es:
C.M1: -1
C.M2: -2
C.M1: 10
C.M2: 10
C.M1: 20
C.M2: 20
C.M1: 20
C.M1: 30
C.M2: 30
C.M1: 30
C.M3: 30
C.M1: 40
C.M2: 40
C.M3: 40
C.M1: 50
C.M2: 50
C.M1: 60
C.M1: 60
Excepciones
18/09/2021 • 9 minutes to read
Las excepciones en C# proporcionan una forma estructurada, uniforme y con seguridad de tipos de controlar las
condiciones de error del nivel de sistema y de la aplicación. El mecanismo de excepción en C# es muy similar al
de C++, con algunas diferencias importantes:
En C#, todas las excepciones deben estar representadas por una instancia de un tipo de clase derivado de
System.Exception . En C++, cualquier valor de cualquier tipo se puede utilizar para representar una
excepción.
En C#, se puede usar un bloque finally (la instrucción try) para escribir el código de finalización que se
ejecuta en la ejecución normal y en condiciones excepcionales. Este código es difícil de escribir en C++ sin
duplicar el código.
En C#, las excepciones de nivel de sistema, como Overflow, división por cero y desreferencia nula, tienen
clases de excepción bien definidas y se encuentran en un par de condiciones de error de nivel de aplicación.
La clase System.Exception
La System.Exception clase es el tipo base de todas las excepciones. Esta clase tiene algunas propiedades
importantes que comparten todas las excepciones:
Message es una propiedad de solo lectura de tipo string que contiene una descripción inteligible de la
razón de la excepción.
InnerException es una propiedad de solo lectura de tipo Exception . Si su valor no es null, hace referencia a
la excepción que provocó la excepción actual, es decir, la excepción actual se produjo en un bloque catch que
controla el InnerException . De lo contrario, su valor es null, lo que indica que esta excepción no se produjo
por otra excepción. El número de objetos de excepción encadenados de esta manera puede ser arbitrario.
El valor de estas propiedades se puede especificar en llamadas al constructor de instancia para
System.Exception .
Gran parte del lenguaje C# permite al programador especificar información declarativa sobre las entidades
definidas en el programa. Por ejemplo, la accesibilidad de un método en una clase se especifica al decorarlo con
el method_modifier s public , protected , internal y private .
C# permite a los programadores inventar nuevos tipos de información declarativa, denominados atributos .
Después, los programadores pueden asociar atributos a varias entidades de programa y recuperar información
de atributos en un entorno en tiempo de ejecución. Por ejemplo, un marco podría definir un HelpAttribute
atributo que se puede colocar en determinados elementos del programa (como clases y métodos) para
proporcionar una asignación de esos elementos de programa a su documentación.
Los atributos se definen a través de la declaración de clases de atributos (clases de atributos), que pueden tener
parámetros con nombre y de posición (parámetros posicionales y con nombre). Los atributos se asocian a
entidades en un programa de C# mediante especificaciones de atributo (especificación de atributo) y se pueden
recuperar en tiempo de ejecución como instancias de atributo (instancias de atributo).
Clases de atributos
Una clase que deriva de la clase abstracta System.Attribute , ya sea directa o indirectamente, es una clase *
Attribute _. La declaración de una clase de atributo define un nuevo tipo de _ Attribute* que se puede colocar
en una declaración. Por Convención, las clases de atributo se denominan con el sufijo Attribute . Los usos de
un atributo pueden incluir u omitir este sufijo.
Uso de atributos
El atributo AttributeUsage (el atributo AttributeUsage) se usa para describir cómo se puede utilizar una clase de
atributo.
AttributeUsage tiene un parámetro posicional (parámetros posicionales y con nombre) que permite que una
clase de atributo especifique los tipos de declaraciones en las que se puede usar. En el ejemplo
using System;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface)]
public class SimpleAttribute: Attribute
{
...
}
define una clase de atributo denominada SimpleAttribute que se puede colocar solo en class_declaration s y
interface_declaration s. En el ejemplo
muestra varios usos del Simple atributo. Aunque este atributo se define con el nombre SimpleAttribute ,
cuando se utiliza este atributo, Attribute se puede omitir el sufijo, lo que da lugar a un nombre corto Simple .
Por lo tanto, el ejemplo anterior es semánticamente equivalente a lo siguiente:
[SimpleAttribute] class Class1 {...}
AttributeUsage tiene un parámetro con nombre (parámetros posicionales y con nombre) denominado
AllowMultiple , que indica si el atributo se puede especificar más de una vez para una entidad determinada. Si
AllowMultiple para una clase de atributo es true, esa clase de atributo es una clase de atributo * de uso
múltiple _ y se puede especificar más de una vez en una entidad. Si AllowMultiple para una clase de atributo
es false o no se especifica, esa clase de atributo es una clase de atributo de un solo uso* * y se puede especificar
como máximo una vez en una entidad.
En el ejemplo
using System;
muestra una declaración de clase con dos usos del Author atributo.
AttributeUsage tiene otro parámetro con nombre llamado Inherited , que indica si el atributo, cuando se
especifica en una clase base, también es heredado por las clases que derivan de esa clase base. Si Inherited
para una clase de atributo es true, se hereda ese atributo. Si Inherited para una clase de atributo es false, ese
atributo no se hereda. Si no se especifica, su valor predeterminado es true.
Una clase de atributo que X no tiene un AttributeUsage atributo asociado, como en
using System;
es equivalente a lo siguiente:
using System;
[AttributeUsage(
AttributeTargets.All,
AllowMultiple = false,
Inherited = true)
]
class X: Attribute {...}
using System;
[AttributeUsage(AttributeTargets.Class)]
public class HelpAttribute: Attribute
{
public HelpAttribute(string url) { // Positional parameter
...
}
define una clase de atributo denominada HelpAttribute que tiene un parámetro posicional, url , y un
parámetro con nombre, Topic . Aunque no es estático y público, la propiedad no Url define un parámetro con
nombre, ya que no es de lectura y escritura.
Esta clase de atributo se puede utilizar como sigue:
[Help("http://www.mycompany.com/.../Class1.htm")]
class Class1
{
...
}
Especificación de atributo
*Especificación de atributo _ es la aplicación de un atributo definido previamente a una declaración. Un
atributo es un fragmento de información declarativa adicional que se especifica para una declaración. Los
atributos se pueden especificar en el ámbito global (para especificar atributos en el ensamblado o módulo
contenedor) y para _type_declaration * s (declaraciones de tipos). class_member_declaration s (restricciones de
parámetros de tipo), interface_member_declaration s (miembros de interfaz), struct_member_declaration s
(miembros de struct), enum_member_declaration s (miembros de enumeración), accessor_declarations
(descriptores de acceso), event_accessor_declarations (eventos similares a campos) y formal_parameter_list s
(parámetros de método).
Los atributos se especifican en las secciones de atributos . Una sección de atributos consta de un par de
corchetes, que rodean una lista separada por comas de uno o más atributos. El orden en que se especifican los
atributos en esta lista y el orden en el que se organizan las secciones asociadas a la misma entidad de programa
no es significativo. Por ejemplo, las especificaciones de atributo [A][B] ,, [B][A] [A,B] y [B,A] son
equivalentes.
global_attributes
: global_attribute_section+
;
global_attribute_section
: '[' global_attribute_target_specifier attribute_list ']'
| '[' global_attribute_target_specifier attribute_list ',' ']'
;
global_attribute_target_specifier
: global_attribute_target ':'
;
global_attribute_target
: 'assembly'
| 'module'
;
attributes
: attribute_section+
;
attribute_section
: '[' attribute_target_specifier? attribute_list ']'
| '[' attribute_target_specifier? attribute_list ',' ']'
;
attribute_target_specifier
: attribute_target ':'
;
attribute_target
: 'field'
| 'event'
| 'method'
| 'param'
| 'property'
| 'return'
| 'type'
;
attribute_list
: attribute (',' attribute)*
;
attribute
: attribute_name attribute_arguments?
;
attribute_name
: type_name
;
attribute_arguments
: '(' positional_argument_list? ')'
| '(' positional_argument_list ',' named_argument_list ')'
| '(' named_argument_list ')'
;
positional_argument_list
: positional_argument (',' positional_argument)*
;
positional_argument
: attribute_argument_expression
;
named_argument_list
: named_argument (',' named_argument)*
;
named_argument
: identifier '=' attribute_argument_expression
;
attribute_argument_expression
: expression
;
Un atributo consta de un attribute_name y una lista opcional de argumentos posicionales y con nombre. Los
argumentos posicionales (si existen) preceden a los argumentos con nombre. Un argumento posicional consta
de un attribute_argument_expression; un argumento con nombre se compone de un nombre seguido de un
signo igual, seguido de una attribute_argument_expression, que, juntos, están restringidas por las mismas
reglas que la asignación simple. El orden de los argumentos con nombre no es significativo.
El attribute_name identifica una clase de atributo. Si el formato de attribute_name es type_name , este nombre
debe hacer referencia a una clase de atributo. De lo contrario, se produce un error en tiempo de compilación. En
el ejemplo
class Class1 {}
produce un error en tiempo de compilación porque intenta utilizar Class1 como una clase de atributos cuando
Class1 no es una clase de atributos.
[Author("Dennis Ritchie")]
class Class2 {}
Es un error especificar una attribute_target_specifier no válida. Por ejemplo, el especificador param no se puede
usar en una declaración de clase:
Por Convención, las clases de atributo se denominan con el sufijo Attribute . Un attribute_name del formulario
type_name puede incluir u omitir este sufijo. Si se encuentra una clase de atributo con y sin este sufijo, existe
una ambigüedad y se produce un error en tiempo de compilación. Si el attribute_name está escrito de modo
que su identificador situado más a la derecha sea un identificador textual (identificadores), solo se hace coincidir
un atributo sin sufijo, lo que permite resolver este tipo de ambigüedad. En el ejemplo
using System;
[AttributeUsage(AttributeTargets.All)]
public class X: Attribute
{}
[AttributeUsage(AttributeTargets.All)]
public class XAttribute: Attribute
{}
[@X] // Refers to X
class Class3 {}
muestra dos clases de atributos denominadas X y XAttribute . El atributo [X] es ambiguo, ya que podría
hacer referencia a X o XAttribute . El uso de un identificador textual permite especificar la intención exacta en
estos casos raros. El atributo [XAttribute] no es ambiguo (aunque sería si hubiera una clase de atributo
denominada XAttributeAttribute !). Si se quita la declaración de la clase X , ambos atributos hacen referencia
a la clase de atributo denominada XAttribute , como se indica a continuación:
using System;
[AttributeUsage(AttributeTargets.All)]
public class XAttribute: Attribute
{}
Es un error en tiempo de compilación utilizar una clase de atributo de un solo uso más de una vez en la misma
entidad. En el ejemplo
using System;
[AttributeUsage(AttributeTargets.Class)]
public class HelpStringAttribute: Attribute
{
string value;
[HelpString("Description of Class1")]
[HelpString("Another description of Class1")]
public class Class1 {}
produce un error en tiempo de compilación porque intenta utilizar HelpString , que es una clase de atributo de
un solo uso, más de una vez en la declaración de Class1 .
Una expresión E es una attribute_argument_expression si se cumplen todas las instrucciones siguientes:
El tipo de E es un tipo de parámetro de atributo (tipos de parámetro de atributo).
En tiempo de compilación, el valor de E se puede resolver como uno de los siguientes:
Valor constante
Un objeto System.Type .
Matriz unidimensional de attribute_argument_expression s.
Por ejemplo:
using System;
[AttributeUsage(AttributeTargets.Class)]
public class TestAttribute: Attribute
{
public int P1 {
get {...}
set {...}
}
public Type P2 {
get {...}
set {...}
}
public object P3 {
get {...}
set {...}
}
}
Una typeof_expression (el operador typeof) utilizada como expresión de argumento de atributo puede hacer
referencia a un tipo no genérico, un tipo construido cerrado o un tipo genérico sin enlazar, pero no puede hacer
referencia a un tipo abierto. Esto es para asegurarse de que la expresión se puede resolver en tiempo de
compilación.
class A: Attribute
{
public A(Type t) {...}
}
class G<T>
{
[A(typeof(T))] T t; // Error, open type in attribute
}
class X
{
[A(typeof(List<int>))] int x; // Ok, closed constructed type
[A(typeof(List<>))] int y; // Ok, unbound generic type
}
Instancias de atributos
Una instancia de atributo es una instancia de que representa un atributo en tiempo de ejecución. Un atributo
se define con una clase de atributo, argumentos posicionales y argumentos con nombre. Una instancia de
atributo es una instancia de la clase de atributos que se inicializa con los argumentos posicionales y con
nombre.
La recuperación de una instancia de atributo implica el procesamiento en tiempo de compilación y en tiempo de
ejecución, como se describe en las secciones siguientes.
Compilación de un atributo
La compilación de un atributo con la clase de atributo T , positional_argument_list P y named_argument_list
N , consta de los siguientes pasos:
Siga los pasos de procesamiento en tiempo de compilación para compilar un object_creation_expression del
formulario new T(P) . Estos pasos dan como resultado un error en tiempo de compilación o determinan un
constructor C de instancia en T que se puede invocar en tiempo de ejecución.
Si no C tiene accesibilidad pública, se produce un error en tiempo de compilación.
Para cada named_argument Arg en N :
Supongamos que Name es el identificador de la named_argument Arg .
Name debe identificar una propiedad o un campo público de lectura/escritura no estático en T . Si T
no tiene este campo o propiedad, se produce un error en tiempo de compilación.
Mantenga la siguiente información para la creación de instancias en tiempo de ejecución del atributo: la clase
de atributo T , el constructor C de instancia en T , el positional_argument_list P y el
named_argument_list N .
Recuperación en tiempo de ejecución de una instancia de atributo
La compilación de un atributo produce una clase de atributo T , un constructor C de instancia en T , un
positional_argument_list P y un named_argument_list N . Dada esta información, una instancia de atributo se
puede recuperar en tiempo de ejecución mediante los pasos siguientes:
Siga los pasos de procesamiento en tiempo de ejecución para ejecutar una object_creation_expression del
formulario new T(P) , mediante el constructor de instancia, C tal y como se determina en tiempo de
compilación. Estos pasos dan como resultado una excepción o producen una instancia O de T .
Para cada named_argument Arg en N , en orden:
Supongamos que Name es el identificador de la named_argument Arg . Si Name no identifica una
propiedad o un campo de lectura/escritura público no estático en O , se produce una excepción.
Supongamos Value el resultado de evaluar el attribute_argument_expression de Arg .
Si Name identifica un campo en O , establezca este campo en Value .
De lo contrario, Name identifica una propiedad en O . Establezca esta propiedad en Value .
El resultado es O , una instancia de la clase de atributos que se ha T inicializado con el
positional_argument_list P y el named_argument_list N .
Atributos reservados
Un pequeño número de atributos afecta al lenguaje de alguna manera. Estos atributos incluyen lo siguiente:
System.AttributeUsageAttribute (El atributo AttributeUsage), que se usa para describir las maneras en las
que se puede usar una clase de atributo.
System.Diagnostics.ConditionalAttribute (El atributo Conditional), que se usa para definir métodos
condicionales.
System.ObsoleteAttribute (El atributo obsoleto), que se usa para marcar un miembro como obsoleto.
System.Runtime.CompilerServices.CallerLineNumberAttribute ,
System.Runtime.CompilerServices.CallerFilePathAttribute y
System.Runtime.CompilerServices.CallerMemberNameAttribute (atributos de información del llamador), que se
usan para proporcionar información sobre el contexto de llamada a los parámetros opcionales.
El atributo AttributeUsage
El atributo AttributeUsage se utiliza para describir la forma en que se puede usar la clase de atributo.
Una clase que se decora con el AttributeUsage atributo debe derivarse de System.Attribute , ya sea directa o
indirectamente. De lo contrario, se produce un error en tiempo de compilación.
namespace System
{
[AttributeUsage(AttributeTargets.Class)]
public class AttributeUsageAttribute: Attribute
{
public AttributeUsageAttribute(AttributeTargets validOn) {...}
public virtual bool AllowMultiple { get {...} set {...} }
public virtual bool Inherited { get {...} set {...} }
public virtual AttributeTargets ValidOn { get {...} }
}
El atributo Conditional
El atributo Conditional habilita la definición de *métodos condicionales _ y _ clases de atributos
condicionales *.
namespace System.Diagnostics
{
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true)]
public class ConditionalAttribute: Attribute
{
public ConditionalAttribute(string conditionString) {...}
public string ConditionString { get {...} }
}
}
Métodos condicionales
Un método decorado con el Conditional atributo es un método condicional. El Conditional atributo indica una
condición mediante la prueba de un símbolo de compilación condicional. Las llamadas a un método condicional
se incluyen o se omiten en función de si este símbolo se define en el punto de la llamada. Si se define el
símbolo, se incluye la llamada; de lo contrario, se omite la llamada (incluida la evaluación del receptor y los
parámetros de la llamada).
Un método condicional está sujeto a las siguientes restricciones:
El método condicional debe ser un método en un class_declaration o struct_declaration. Se produce un error
en tiempo de compilación si el Conditional atributo se especifica en un método en una declaración de
interfaz.
El método condicional debe tener un tipo de valor devuelto de void .
El método condicional no debe estar marcado con el override modificador. Sin embargo, un método
condicional se puede marcar con el virtual modificador. Las invalidaciones de este tipo de método son
implícitamente condicionales y no deben marcarse explícitamente con un Conditional atributo.
El método condicional no debe ser una implementación de un método de interfaz. De lo contrario, se
produce un error en tiempo de compilación.
Además, se produce un error en tiempo de compilación si se utiliza un método condicional en una
delegate_creation_expression. En el ejemplo
#define DEBUG
using System;
using System.Diagnostics;
class Class1
{
[Conditional("DEBUG")]
public static void M() {
Console.WriteLine("Executed Class1.M");
}
}
class Class2
{
public static void Test() {
Class1.M();
}
}
declara Class1.M como un método condicional. Class2``Test el método de llama a este método. Dado que se
define el símbolo de compilación condicional DEBUG , si Class2.Test se llama a, se llamará a M . Si DEBUG no
se ha definido el símbolo, Class2.Test no llamará a Class1.M .
Es importante tener en cuenta que la inclusión o exclusión de una llamada a un método condicional se controla
mediante los símbolos de compilación condicional en el punto de la llamada. En el ejemplo
Archivo class1.cs :
using System.Diagnostics;
class Class1
{
[Conditional("DEBUG")]
public static void F() {
Console.WriteLine("Executed Class1.F");
}
}
Archivo class2.cs :
#define DEBUG
class Class2
{
public static void G() {
Class1.F(); // F is called
}
}
Archivo class3.cs :
#undef DEBUG
class Class3
{
public static void H() {
Class1.F(); // F is not called
}
}
las clases Class2 y Class3 cada una contienen llamadas al método condicional Class1.F , que es condicional
en función de si se ha definido o no DEBUG . Puesto que este símbolo se define en el contexto de Class2 pero
no Class3 , se incluye la llamada a F en Class2 , mientras que la llamada a F en Class3 se omite.
El uso de métodos condicionales en una cadena de herencia puede ser confuso. Las llamadas realizadas a un
método condicional a través base de, del formulario base.M , están sujetas a las reglas de llamada de método
condicional normales. En el ejemplo
Archivo class1.cs :
using System;
using System.Diagnostics;
class Class1
{
[Conditional("DEBUG")]
public virtual void M() {
Console.WriteLine("Class1.M executed");
}
}
Archivo class2.cs :
using System;
Archivo class3.cs :
#define DEBUG
using System;
class Class3
{
public static void Test() {
Class2 c = new Class2();
c.M(); // M is called
}
}
Class2 incluye una llamada al M definido en su clase base. Esta llamada se omite porque el método base es
condicional en función de la presencia del símbolo DEBUG , que no está definida. Por lo tanto, el método solo
escribe en la consola " Class2.M executed ". El uso prudente de pp_declaration s puede eliminar tales
problemas.
Clases de atributos condicionales
Una clase de atributo (clases de atributos) decorada con uno o varios Conditional atributos es una clase de
atributo condicional . Por tanto, una clase de atributo condicional está asociada a los símbolos de compilación
condicional declarados en sus Conditional atributos. En este ejemplo:
using System;
using System.Diagnostics;
[Conditional("ALPHA")]
[Conditional("BETA")]
public class TestAttribute : Attribute {}
declara TestAttribute como una clase de atributo condicional asociada a los símbolos de compilaciones
condicionales ALPHA y BETA .
Las especificaciones de atributo (especificación de atributo) de un atributo condicional se incluyen si uno o
varios de sus símbolos de compilación condicional asociados se definen en el punto de especificación; de lo
contrario, se omite la especificación de atributo.
Es importante tener en cuenta que la inclusión o exclusión de una especificación de atributo de una clase de
atributo condicional se controla mediante los símbolos de compilación condicional en el punto de la
especificación. En el ejemplo
Archivo test.cs :
using System;
using System.Diagnostics;
[Conditional("DEBUG")]
Archivo class1.cs :
#define DEBUG
class Class1 {}
Archivo class2.cs :
#undef DEBUG
class Class2 {}
cada una de las clases Class1 y Class2 se decoran con el atributo Test , que es condicional en función de si
se ha definido o no DEBUG . Puesto que este símbolo se define en el contexto de Class1 pero no Class2 , se
incluye la especificación del Test atributo en Class1 , mientras que la especificación del Test atributo en
Class2 se omite.
El atributo Obsolete
El atributo Obsolete se usa para marcar tipos y miembros de tipos que ya no se deben usar.
namespace System
{
[AttributeUsage(
AttributeTargets.Class |
AttributeTargets.Struct |
AttributeTargets.Enum |
AttributeTargets.Interface |
AttributeTargets.Delegate |
AttributeTargets.Method |
AttributeTargets.Constructor |
AttributeTargets.Property |
AttributeTargets.Field |
AttributeTargets.Event,
Inherited = false)
]
public class ObsoleteAttribute: Attribute
{
public ObsoleteAttribute() {...}
public ObsoleteAttribute(string message) {...}
public ObsoleteAttribute(string message, bool error) {...}
public string Message { get {...} }
public bool IsError { get {...} }
}
}
Si un programa utiliza un tipo o un miembro que se decora con el Obsolete atributo, el compilador emite una
advertencia o un error. En concreto, el compilador emite una advertencia si no se proporciona ningún
parámetro de error, o si se proporciona el parámetro de error y tiene el valor false . El compilador emite un
error si se especifica el parámetro de error y tiene el valor true .
En el ejemplo
class B
{
public void F() {}
}
class Test
{
static void Main() {
A a = new A(); // Warning
a.F();
}
}
la clase A se decora con el Obsolete atributo. Cada uso de A en Main genera una advertencia que incluye el
mensaje especificado, "esta clase está obsoleta; Use la clase B en su lugar ".
Atributos de información del llamador
Para fines como registro e informes, a veces resulta útil que un miembro de función obtenga determinada
información en tiempo de compilación sobre el código de llamada. Los atributos de información del llamador
proporcionan una manera de pasar tal información de forma transparente.
Cuando un parámetro opcional se anota con uno de los atributos de información del llamador, la omisión del
argumento correspondiente en una llamada no hace que el valor del parámetro predeterminado se sustituya. En
su lugar, si la información especificada sobre el contexto de la llamada está disponible, esa información se
pasará como el valor del argumento.
Por ejemplo:
using System.Runtime.CompilerServices
...
Una llamada a Log() sin argumentos imprimiría el número de línea y la ruta de acceso del archivo de la
llamada, así como el nombre del miembro en el que se produjo la llamada.
Los atributos de información del llamador se pueden producir en parámetros opcionales en cualquier parte,
incluidas en las declaraciones de delegado. Sin embargo, los atributos de información del llamador específico
tienen restricciones en los tipos de los parámetros que pueden atribuir, de modo que siempre habrá una
conversión implícita de un valor sustituido al tipo de parámetro.
Es un error tener el mismo atributo de información de llamador en un parámetro de la parte de definición e
implementación de una declaración de método parcial. Solo se aplican los atributos de información de llamador
en el elemento de definición, mientras que los atributos de información de llamador que se producen solo en la
parte de implementación se omiten.
La información del llamador no afecta a la resolución de sobrecarga. Como los parámetros opcionales con
atributos se siguen omitiendo del código fuente del llamador, la resolución de sobrecarga omite esos
parámetros de la misma manera que omite otros parámetros opcionales omitidos (resolución de sobrecarga).
La información del llamador solo se sustituye cuando una función se invoca explícitamente en el código fuente.
Las invocaciones implícitas como las llamadas a un constructor primario implícito no tienen una ubicación de
origen y no sustituyen la información del llamador. Además, las llamadas que se enlazan dinámicamente no
sustituirán la información del llamador. Cuando se omite un parámetro con atributos de información de
llamador en estos casos, se utiliza en su lugar el valor predeterminado especificado del parámetro.
Una excepción son las expresiones de consulta. Se consideran expansiones sintácticas y, si las llamadas se
expanden para omitir los parámetros opcionales con atributos de información de llamador, se sustituirá la
información del llamador. La ubicación usada es la ubicación de la cláusula de consulta a partir de la cual se
generó la llamada.
Si se especifica más de un atributo de información de llamador en un parámetro determinado, se prefieren en el
orden siguiente: CallerLineNumber , CallerFilePath , CallerMemberName .
El atributo CallerLineNumber
System.Runtime.CompilerServices.CallerLineNumberAttribute Se permite en parámetros opcionales cuando hay
una conversión implícita estándar (conversiones implícitas estándar) del valor constante int.MaxValue al tipo
del parámetro. Esto garantiza que cualquier número de línea no negativo hasta ese valor se puede pasar sin
errores.
Si una invocación de función de una ubicación en el código fuente omite un parámetro opcional con
CallerLineNumberAttribute , se usa un literal numérico que representa el número de línea de la ubicación como
argumento para la invocación en lugar del valor de parámetro predeterminado.
Si la invocación abarca varias líneas, la línea elegida depende de la implementación.
Tenga en cuenta que el número de línea puede verse afectado por las #line directivas (directivas de línea).
El atributo CallerFilePath
System.Runtime.CompilerServices.CallerFilePathAttribute Se permite en parámetros opcionales cuando hay una
conversión implícita estándar (conversiones implícitas estándar) de string en el tipo del parámetro.
Si una invocación de función de una ubicación en el código fuente omite un parámetro opcional con
CallerFilePathAttribute , entonces se usa un literal de cadena que representa la ruta de acceso del archivo de
la ubicación como argumento para la invocación en lugar del valor del parámetro predeterminado.
El formato de la ruta de acceso del archivo depende de la implementación.
Tenga en cuenta que la ruta de acceso al archivo puede verse afectada por las #line directivas (directivas de
línea).
El atributo CallerMemberName
System.Runtime.CompilerServices.CallerMemberNameAttribute Se permite en parámetros opcionales cuando hay
una conversión implícita estándar (conversiones implícitas estándar) de string en el tipo del parámetro.
Si una invocación de función desde una ubicación dentro del cuerpo de un miembro de función o dentro de un
atributo aplicado al propio miembro de la función o a su tipo de valor devuelto, parámetros o parámetros de
tipo en el código fuente omite un parámetro opcional con CallerMemberNameAttribute , se usa un literal de
cadena que representa el nombre de ese miembro como argumento para la invocación en lugar del valor del
parámetro predeterminado
En el caso de las invocaciones que se producen dentro de métodos genéricos, solo se usa el nombre del método
en sí, sin la lista de parámetros de tipo.
En el caso de las invocaciones que se producen dentro de implementaciones explícitas de miembros de interfaz,
solo se usa el nombre del método en sí, sin la calificación de interfaz anterior.
En el caso de las invocaciones que se producen dentro de los descriptores de acceso de propiedades o eventos,
el nombre de miembro utilizado es el de la propiedad o el propio evento.
En el caso de las invocaciones que se producen dentro de los descriptores de acceso del indizador, el nombre de
miembro utilizado es el proporcionado por un IndexerNameAttribute (el atributo IndexerName) en el miembro
del indexador, si está presente, o el nombre predeterminado Item en caso contrario.
En el caso de las invocaciones que se producen dentro de las declaraciones de constructores de instancias,
constructores estáticos, destructores y operadores, el nombre de miembro utilizado depende de la
implementación.
namespace System.Runtime.CompilerServices.CSharp
{
[AttributeUsage(AttributeTargets.Property)]
public class IndexerNameAttribute: Attribute
{
public IndexerNameAttribute(string indexerName) {...}
public string Value { get {...} }
}
}
Código no seguro
18/09/2021 • 74 minutes to read
El lenguaje C# básico, tal como se define en los capítulos anteriores, difiere principalmente de C y C++ en su
omisión de punteros como un tipo de datos. En su lugar, C# proporciona referencias y la capacidad de crear
objetos administrados por un recolector de elementos no utilizados. Este diseño, junto con otras características,
hace que C# sea un lenguaje mucho más seguro que C o C++. En el lenguaje C# básico, simplemente no es
posible tener una variable no inicializada, un puntero "pendiente" o una expresión que indiza una matriz más
allá de sus límites. Por lo tanto, se eliminan todas las categorías de errores que suelen plagar programas de C y
C++.
Aunque prácticamente todas las construcciones de tipo de puntero en C o C++ tienen un tipo de referencia
homólogo en C#, sin embargo, hay situaciones en las que el acceso a los tipos de puntero se convierte en una
necesidad. Por ejemplo, la interacción con el sistema operativo subyacente, el acceso a un dispositivo asignado a
la memoria o la implementación de un algoritmo crítico en el tiempo puede no ser posible ni práctico sin acceso
a los punteros. Para satisfacer esta necesidad, C# ofrece la posibilidad de escribir código no seguro .
En código no seguro es posible declarar y operar en punteros, para realizar conversiones entre punteros y tipos
enteros, para tomar la dirección de las variables, etc. En cierto sentido, escribir código no seguro es muy similar
a escribir código de C en un programa de C#.
En realidad, el código no seguro es una característica "segura" desde la perspectiva de los desarrolladores y los
usuarios. El código no seguro debe estar claramente marcado con el modificador unsafe , por lo que los
desarrolladores no pueden usar características no seguras accidentalmente, y el motor de ejecución funciona
para asegurarse de que el código no seguro no se puede ejecutar en un entorno que no es de confianza.
Contextos no seguros
Las características no seguras de C# solo están disponibles en contextos no seguros. Un contexto no seguro se
introduce mediante la inclusión de un unsafe modificador en la declaración de un tipo o miembro, o mediante
la utilización de un unsafe_statement:
Una declaración de una clase, un struct, una interfaz o un delegado puede incluir un unsafe modificador, en
cuyo caso toda la extensión textual de esa declaración de tipos (incluido el cuerpo de la clase, el struct o la
interfaz) se considera un contexto no seguro.
Una declaración de un campo, un método, una propiedad, un evento, un indexador, un operador, un
constructor de instancia, un destructor o un constructor estático puede incluir un unsafe modificador, en
cuyo caso toda la extensión textual de dicha declaración de miembro se considera un contexto no seguro.
Un unsafe_statement habilita el uso de un contexto no seguro dentro de un bloque. Toda la extensión textual
del bloque asociado se considera un contexto no seguro.
A continuación se muestran las producciones de gramática asociadas.
class_modifier_unsafe
: 'unsafe'
;
struct_modifier_unsafe
: 'unsafe'
;
interface_modifier_unsafe
: 'unsafe'
;
delegate_modifier_unsafe
: 'unsafe'
;
field_modifier_unsafe
: 'unsafe'
;
method_modifier_unsafe
: 'unsafe'
;
property_modifier_unsafe
: 'unsafe'
;
event_modifier_unsafe
: 'unsafe'
;
indexer_modifier_unsafe
: 'unsafe'
;
operator_modifier_unsafe
: 'unsafe'
;
constructor_modifier_unsafe
: 'unsafe'
;
destructor_declaration_unsafe
: attributes? 'extern'? 'unsafe'? '~' identifier '(' ')' destructor_body
| attributes? 'unsafe'? 'extern'? '~' identifier '(' ')' destructor_body
;
static_constructor_modifiers_unsafe
: 'extern'? 'unsafe'? 'static'
| 'unsafe'? 'extern'? 'static'
| 'extern'? 'static' 'unsafe'?
| 'unsafe'? 'static' 'extern'?
| 'static' 'extern'? 'unsafe'?
| 'static' 'unsafe'? 'extern'?
;
embedded_statement_unsafe
: unsafe_statement
| fixed_statement
;
unsafe_statement
: 'unsafe' block
;
En el ejemplo
el unsafe modificador especificado en la declaración de estructura hace que la extensión textual completa de la
declaración de estructura se convierta en un contexto no seguro. Por lo tanto, es posible declarar los Left
Right campos y para que sean de un tipo de puntero. También se puede escribir el ejemplo anterior.
Aquí, los unsafe modificadores de las declaraciones de campo hacen que esas declaraciones se consideren
contextos no seguros.
Además de establecer un contexto no seguro, lo que permite el uso de tipos de puntero, el unsafe modificador
no tiene ningún efecto en un tipo o miembro. En el ejemplo
public class A
{
public unsafe virtual void F() {
char* p;
...
}
}
public class B: A
{
public override void F() {
base.F();
...
}
}
el unsafe modificador del F método en A simplemente hace que la extensión textual de se F convierta en
un contexto no seguro en el que se pueden usar las características no seguras del lenguaje. En la invalidación de
F en B , no es necesario volver a especificar el unsafe modificador, a menos que, por supuesto, el F método
de B sí mismo necesite acceso a características no seguras.
La situación es ligeramente diferente cuando un tipo de puntero forma parte de la Signatura del método.
public class B: A
{
public unsafe override void F(char* p) {...}
}
Aquí, dado F que la firma de incluye un tipo de puntero, solo se puede escribir en un contexto no seguro. Sin
embargo, el contexto no seguro se puede introducir haciendo que la clase completa sea insegura, como es el
caso en A , o incluyendo un unsafe modificador en la declaración del método, como es el caso B de.
Tipos de puntero
En un contexto no seguro, un tipo (tipos) puede ser un pointer_type , así como un value_type o un
reference_type. Sin embargo, un pointer_type también puede usarse en una typeof expresión (expresiones de
creación de objetos anónimos) fuera de un contexto no seguro, ya que dicho uso no es seguro.
type_unsafe
: pointer_type
;
pointer_type
: unmanaged_type '*'
| 'void' '*'
;
unmanaged_type
: type
;
El tipo especificado antes de * en un tipo de puntero se denomina tipo referente del tipo de puntero.
Representa el tipo de la variable a la que apunta un valor del tipo de puntero.
A diferencia de las referencias (valores de tipos de referencia), el recolector de elementos no utilizados no realiza
un seguimiento de los punteros; el recolector de elementos no utilizados no tiene conocimiento de los punteros
y los datos a los que apuntan. Por esta razón, no se permite que un puntero señale a una referencia o a un struct
que contenga referencias, y el tipo referente de un puntero debe ser un unmanaged_type.
Un unmanaged_type es cualquier tipo que no sea un tipo reference_type o construido y que no contenga
campos de tipo reference_type o construido en ningún nivel de anidamiento. En otras palabras, una
unmanaged_type es una de las siguientes:
sbyte, byte , short , ushort , int , uint , long , ulong , char , float , double , decimal o bool .
Cualquier enum_type.
Cualquier pointer_type.
Cualquier struct_type definido por el usuario que no sea un tipo construido y que contenga campos de
unmanaged_type s solamente.
La regla intuitiva para la combinación de punteros y referencias es que los referentes de referencias (objetos)
pueden contener punteros, pero los referentes de punteros no pueden contener referencias.
En la tabla siguiente se proporcionan algunos ejemplos de tipos de puntero:
Para una implementación determinada, todos los tipos de puntero deben tener el mismo tamaño y
representación.
A diferencia de C y C++, cuando se declaran varios punteros en la misma declaración, en C# el * se escribe
junto con el tipo subyacente únicamente, no como un signo de puntuación de prefijo en cada nombre de
puntero. Por ejemplo
El valor de un puntero que tiene un tipo T* representa la dirección de una variable de tipo T . El operador de
direccionamiento indirecto del puntero * (direccionamiento indirecto del puntero) se puede usar para tener
acceso a esta variable. Por ejemplo, dada una variable P de tipo int* , la expresión *P denota la variable que
int se encuentra en la dirección contenida en P .
Al igual que una referencia de objeto, un puntero puede ser null . Al aplicar el operador de direccionamiento
indirecto a un null puntero, se produce un comportamiento definido por la implementación. Un puntero con
valor null se representa mediante All-bits-cero.
El void* tipo representa un puntero a un tipo desconocido. Dado que el tipo referente es desconocido, el
operador de direccionamiento indirecto no se puede aplicar a un puntero de tipo void* , ni se puede realizar
ninguna operación aritmética en dicho puntero. Sin embargo, un puntero de tipo void* se puede convertir a
cualquier otro tipo de puntero (y viceversa).
Los tipos de puntero son una categoría independiente de tipos. A diferencia de los tipos de referencia y los tipos
de valor, los tipos de puntero no heredan de object y no existen conversiones entre tipos de puntero y object
. En concreto, no se admiten las conversiones boxing y unboxing (conversión boxing y unboxing) para los
punteros. Sin embargo, se permiten conversiones entre diferentes tipos de puntero y entre tipos de puntero y
tipos enteros. Esto se describe en conversiones de puntero.
No se puede usar un pointer_type como argumento de tipo (tipos construidos) y se produce un error en la
inferencia de tipos (inferencia de tipos) en llamadas a métodos genéricos que habrían deducido un argumento
de tipo para que sea un tipo de puntero.
Un pointer_type se puede usar como el tipo de un campo volátil (campos volátiles).
Aunque los punteros se pueden pasar ref como out parámetros o, esto puede provocar un comportamiento
indefinido, ya que el puntero se puede establecer para que apunte a una variable local que ya no existe cuando
el método llamado vuelve, o al objeto fijo al que se ha usado para apuntar, ya no se ha corregido. Por ejemplo:
using System;
class Test
{
static int value = 20;
Un método puede devolver un valor de algún tipo y ese tipo puede ser un puntero. Por ejemplo, cuando se
proporciona un puntero a una secuencia contigua de int s, el recuento de elementos de la secuencia y otro
int valor, el método siguiente devuelve la dirección de ese valor en esa secuencia, si se produce una
coincidencia; de lo contrario, devuelve null :
Conversiones de puntero
En un contexto no seguro, el conjunto de conversiones implícitas disponibles (conversiones implícitas) se
extiende para incluir las siguientes conversiones de puntero implícitas:
Desde cualquier pointer_type al tipo void* .
Del null literal a cualquier pointer_type.
Además, en un contexto no seguro, el conjunto de conversiones explícitas disponibles (conversiones explícitas)
se extiende para incluir las siguientes conversiones de puntero explícitas:
Desde cualquier pointer_type a cualquier otro pointer_type.
De sbyte , byte , short , ushort , int , uint , long o ulong a cualquier pointer_type.
Desde cualquier pointer_type a sbyte , byte , short , ushort , int , uint , long o ulong .
Por último, en un contexto no seguro, el conjunto de conversiones implícitas estándar (conversiones implícitas
estándar) incluye la siguiente conversión de puntero:
Desde cualquier pointer_type al tipo void* .
Las conversiones entre dos tipos de puntero nunca cambian el valor del puntero real. En otras palabras, una
conversión de un tipo de puntero a otro no tiene ningún efecto en la dirección subyacente proporcionada por el
puntero.
Cuando un tipo de puntero se convierte en otro, si el puntero resultante no está alineado correctamente para el
tipo señalado, el comportamiento es indefinido si se desreferencia el resultado. En general, el concepto
"correctamente alineado" es transitivo: Si un puntero al tipo A se alinea correctamente para un puntero al tipo
B , que, a su vez, está alineado correctamente para un puntero al tipo C , un puntero al tipo A se alinea
correctamente para un puntero al tipo C .
Considere el siguiente caso en el que se tiene acceso a una variable con un tipo a través de un puntero a un tipo
diferente:
char c = 'A';
char* pc = &c;
void* pv = pc;
int* pi = (int*)pv;
int i = *pi; // undefined
*pi = 123456; // undefined
Cuando un tipo de puntero se convierte en un puntero a byte, el resultado apunta al byte más bajo
direccionable de la variable. Los incrementos sucesivos del resultado, hasta el tamaño de la variable,
proporcionan punteros a los bytes restantes de esa variable. Por ejemplo, el siguiente método muestra cada uno
de los ocho bytes en un tipo Double como un valor hexadecimal:
using System;
class Test
{
unsafe static void Main() {
double d = 123.456e23;
unsafe {
byte* pb = (byte*)&d;
for (int i = 0; i < sizeof(double); ++i)
Console.Write("{0:X2} ", *pb++);
Console.WriteLine();
}
}
}
foreach (V v in x) embedded_statement
donde el tipo de x es un tipo de matriz con el formato T[,,...,] , N es el número de dimensiones menos 1 y
T o V es un tipo de puntero, se expande mediante bucles for anidados como se indica a continuación:
{
T[,,...,] a = x;
for (int i0 = a.GetLowerBound(0); i0 <= a.GetUpperBound(0); i0++)
for (int i1 = a.GetLowerBound(1); i1 <= a.GetUpperBound(1); i1++)
...
for (int iN = a.GetLowerBound(N); iN <= a.GetUpperBound(N); iN++) {
V v = (V)a.GetValue(i0,i1,...,iN);
embedded_statement
}
}
Las variables a , i0 , i1 ,..., iN no se pueden ver ni se puede tener acceso a ellas o a x los
embedded_statement ni a ningún otro código fuente del programa. La variable v es de solo lectura en la
instrucción insertada. Si no hay una conversión explícita (conversiones de puntero) de T (el tipo de elemento)
en V , se genera un error y no se realiza ningún otro paso. Si x tiene el valor null ,
System.NullReferenceException se produce una excepción en tiempo de ejecución.
Punteros en expresiones
En un contexto no seguro, una expresión puede producir un resultado de un tipo de puntero, pero fuera de un
contexto no seguro, es un error en tiempo de compilación para que una expresión sea de un tipo de puntero. En
términos precisos, fuera de un contexto no seguro, se produce un error en tiempo de compilación si algún
simple_name (nombres simples), member_access (acceso a miembros), invocation_expression (expresiones de
invocación) o element_access (acceso a elementos) es de un tipo de puntero.
En un contexto no seguro, las producciones primary_no_array_creation_expression (expresiones primarias) y
unary_expression (operadores unarios) permiten las siguientes construcciones adicionales:
primary_no_array_creation_expression_unsafe
: pointer_member_access
| pointer_element_access
| sizeof_expression
;
unary_expression_unsafe
: pointer_indirection_expression
| addressof_expression
;
pointer_indirection_expression
: '*' unary_expression
;
El operador unario * denota el direccionamiento indirecto del puntero y se usa para obtener la variable a la
que señala un puntero. El resultado de evaluar *P , donde P es una expresión de un tipo de puntero T* , es
una variable de tipo T . Es un error en tiempo de compilación aplicar el operador unario * a una expresión de
tipo void* o a una expresión que no es de un tipo de puntero.
El efecto de aplicar el operador unario * a un null puntero está definido por la implementación. En concreto,
no hay ninguna garantía de que esta operación produzca una excepción System.NullReferenceException .
Si se ha asignado un valor no válido al puntero, el comportamiento del operador unario * es indefinido. Entre
los valores no válidos para desreferenciar un puntero por el operador unario * se encuentra una dirección
alineada incorrectamente para el tipo señalado (vea el ejemplo en conversiones de puntero) y la dirección de
una variable después del final de su período de duración.
A efectos del análisis de asignación definitiva, una variable generada al evaluar una expresión del formulario
*P se considera asignada inicialmente (variables asignadas inicialmente).
pointer_member_access
: primary_expression '->' identifier
;
En un acceso de miembro de puntero del formulario P->I , P debe ser una expresión de un tipo de puntero
distinto de void* y I debe indicar un miembro accesible del tipo al que P apunta.
Un acceso de miembro de puntero del formulario P->I se evalúa exactamente como (*P).I . Para obtener una
descripción del operador de direccionamiento indirecto de puntero ( * ), vea direccionamiento indirecto del
puntero. Para obtener una descripción del operador de acceso a miembros ( . ), consulte acceso a miembros.
En el ejemplo
using System;
struct Point
{
public int x;
public int y;
class Test
{
static void Main() {
Point point;
unsafe {
Point* p = &point;
p->x = 10;
p->y = 20;
Console.WriteLine(p->ToString());
}
}
}
el -> operador se usa para tener acceso a los campos e invocar un método de un struct a través de un puntero.
Dado que la operación P->I es exactamente equivalente a (*P).I , el Main método se podría haber escrito
igualmente correctamente:
class Test
{
static void Main() {
Point point;
unsafe {
Point* p = &point;
(*p).x = 10;
(*p).y = 20;
Console.WriteLine((*p).ToString());
}
}
}
pointer_element_access
: primary_no_array_creation_expression '[' expression ']'
;
En un acceso de elemento de puntero del formulario P[E] , P debe ser una expresión de un tipo de puntero
distinto de void* y E debe ser una expresión que se pueda convertir implícitamente a int , uint , long o
ulong .
Un acceso de elemento de puntero del formulario P[E] se evalúa exactamente como *(P + E) . Para obtener
una descripción del operador de direccionamiento indirecto de puntero ( * ), vea direccionamiento indirecto
del puntero. Para obtener una descripción del operador de adición de puntero ( + ), vea aritmética de punteros.
En el ejemplo
class Test
{
static void Main() {
unsafe {
char* p = stackalloc char[256];
for (int i = 0; i < 256; i++) p[i] = (char)i;
}
}
}
un acceso de elemento de puntero se usa para inicializar el búfer de caracteres en un for bucle. Dado que la
operación P[E] es exactamente equivalente a *(P + E) , el ejemplo se podría haber escrito igualmente
correctamente:
class Test
{
static void Main() {
unsafe {
char* p = stackalloc char[256];
for (int i = 0; i < 256; i++) *(p + i) = (char)i;
}
}
}
El operador de acceso a elementos de puntero no comprueba los errores fuera de los límites y el
comportamiento cuando el acceso a un elemento fuera de los límites es indefinido. Es lo mismo que C y C++.
El operador address-of
Un addressof_expression consta de una y comercial ( & ) seguida de un unary_expression.
addressof_expression
: '&' unary_expression
;
Dada una expresión E que es de un tipo T y se clasifica como una variable fija (variables fijas y móviles), la
construcción &E calcula la dirección de la variable proporcionada por E . El tipo del resultado es T* y se
clasifica como un valor. Se produce un error en tiempo de compilación si E no está clasificado como una
variable, si E está clasificado como una variable local de solo lectura, o si E denota una variable móvil. En el
último caso, se puede usar una instrucción fixed (la instrucción Fixed) para "corregir" temporalmente la variable
antes de obtener su dirección. Como se indica en acceso a miembros, fuera de un constructor de instancia o un
constructor estático para una estructura o clase que define un readonly campo, ese campo se considera un
valor, no una variable. Como tal, no se puede tomar su dirección. Del mismo modo, no se puede tomar la
dirección de una constante.
El & operador no requiere que el argumento esté asignado definitivamente, pero después de una & operación,
la variable a la que se aplica el operador se considera definitivamente asignada en la ruta de acceso de ejecución
en la que se produce la operación. Es responsabilidad del programador asegurarse de que la inicialización
correcta de la variable realmente se realiza en esta situación.
En el ejemplo
using System;
class Test
{
static void Main() {
int i;
unsafe {
int* p = &i;
*p = 123;
}
Console.WriteLine(i);
}
}
i se considera definitivamente asignada después de la &i operación que se usa para inicializar p . La
asignación a *p en vigor se inicializa i , pero la inclusión de esta inicialización es responsabilidad del
programador y no se produciría ningún error en tiempo de compilación si se quitase la asignación.
Las reglas de asignación definitiva para el & operador existen de forma que se pueda evitar la inicialización
redundante de variables locales. Por ejemplo, muchas API externas toman un puntero a una estructura que se
rellena mediante la API. Las llamadas a estas API normalmente pasan la dirección de una variable de struct local
y sin la regla, sería necesaria la inicialización redundante de la variable de estructura.
Incremento y decremento de puntero
En un contexto no seguro, los ++ -- operadores y (operadores deincremento y decremento de postfijo y
operadores de incremento y decremento de prefijo) se pueden aplicar a las variables de puntero de todos los
tipos excepto void* . Por lo tanto, para cada tipo de puntero T* , se definen implícitamente los siguientes
operadores:
Los operadores producen los mismos resultados que x + 1 y x - 1 , respectivamente (aritmética de puntero).
En otras palabras, para una variable de puntero de tipo T* , el ++ operador agrega sizeof(T) a la dirección
contenida en la variable y el -- operador resta sizeof(T) de la dirección contenida en la variable.
Si una operación de incremento o decremento de puntero desborda el dominio del tipo de puntero, el resultado
se define en la implementación, pero no se genera ninguna excepción.
Aritmética de puntero
En un contexto no seguro, los + operadores y (operador de - suma y resta) se pueden aplicar a los valores de
todos los tipos de puntero excepto void* . Por lo tanto, para cada tipo de puntero T* , se definen
implícitamente los siguientes operadores:
T* operator +(T* x, int y);
T* operator +(T* x, uint y);
T* operator +(T* x, long y);
T* operator +(T* x, ulong y);
Dada una expresión P de un tipo de puntero T* y una expresión N de tipo int , uint , long o ulong , las
expresiones P + N y N + P calculan el valor de puntero de tipo T* que es el resultado de agregar
N * sizeof(T) a la dirección especificada por P . Del mismo modo, la expresión P - N calcula el valor de
puntero de tipo T* que es el resultado de restar N * sizeof(T) de la dirección proporcionada por P .
Dadas dos expresiones, P y Q , de un tipo de puntero T* , la expresión P - Q calcula la diferencia entre las
direcciones proporcionadas por P y Q y, a continuación, divide esa diferencia por sizeof(T) . El tipo del
resultado es siempre long . En efecto, P - Q se calcula como ((long)(P) - (long)(Q)) / sizeof(T) .
Por ejemplo:
using System;
class Test
{
static void Main() {
unsafe {
int* values = stackalloc int[20];
int* p = &values[1];
int* q = &values[15];
Console.WriteLine("p - q = {0}", p - q);
Console.WriteLine("q - p = {0}", q - p);
}
}
}
p - q = -14
q - p = 14
Si una operación aritmética de puntero desborda el dominio del tipo de puntero, el resultado se trunca en un
modo definido por la implementación, pero no se genera ninguna excepción.
Comparación de punteros
En un contexto no seguro, los == != operadores,, < ,, > <= y => (operadores relacionales y de prueba de
tipos) se pueden aplicar a los valores de todos los tipos de puntero. Los operadores de comparación de
punteros son:
bool operator ==(void* x, void* y);
bool operator !=(void* x, void* y);
bool operator <(void* x, void* y);
bool operator >(void* x, void* y);
bool operator <=(void* x, void* y);
bool operator >=(void* x, void* y);
Dado que existe una conversión implícita de cualquier tipo de puntero al void* tipo, se pueden comparar los
operandos de cualquier tipo de puntero mediante estos operadores. Los operadores de comparación comparan
las direcciones proporcionadas por los dos operandos como si fueran enteros sin signo.
El operador sizeof
El operador sizeof devuelve el número de bytes ocupados por una variable de un tipo determinado. El tipo
especificado como operando sizeof debe ser un unmanaged_type (tipos de puntero).
sizeof_expression
: 'sizeof' '(' unmanaged_type ')'
;
El resultado del sizeof operador es un valor de tipo int . Para determinados tipos predefinidos, el sizeof
operador produce un valor constante, tal como se muestra en la tabla siguiente.
sizeof(sbyte) 1
sizeof(byte) 1
sizeof(short) 2
sizeof(ushort) 2
sizeof(int) 4
sizeof(uint) 4
sizeof(long) 8
sizeof(ulong) 8
sizeof(char) 2
sizeof(float) 4
sizeof(double) 8
sizeof(bool) 1
En el caso de todos los demás tipos, el resultado del sizeof operador está definido por la implementación y se
clasifica como un valor, no como una constante.
No se especifica el orden en que los miembros se empaquetan en un struct.
A efectos de alineación, puede haber un relleno sin nombre al principio de un struct, dentro de un struct y al
final de la estructura. El contenido de los bits usados como relleno es indeterminado.
Cuando se aplica a un operando que tiene el tipo de estructura, el resultado es el número total de bytes en una
variable de ese tipo, incluido el relleno.
fixed (instrucción)
En un contexto no seguro, la producción de embedded_statement (instrucciones) permite una construcción
adicional, la fixed instrucción, que se usa para "corregir" una variable móvil de modo que su dirección
permanece constante mientras dure la instrucción.
fixed_statement
: 'fixed' '(' pointer_type fixed_pointer_declarators ')' embedded_statement
;
fixed_pointer_declarators
: fixed_pointer_declarator (',' fixed_pointer_declarator)*
;
fixed_pointer_declarator
: identifier '=' fixed_pointer_initializer
;
fixed_pointer_initializer
: '&' variable_reference
| expression
;
Cada fixed_pointer_declarator declara una variable local del pointer_type determinado e inicializa esa variable
local con la dirección calculada por el fixed_pointer_initializer correspondiente. Una variable local declarada en
una fixed instrucción es accesible en cualquier fixed_pointer_initializer s que se encuentra a la derecha de la
declaración de la variable y en el embedded_statement de la fixed instrucción. Una variable local declarada por
una fixed instrucción se considera de solo lectura. Se produce un error en tiempo de compilación si la
instrucción incrustada intenta modificar esta variable local (a través de la asignación o los ++ -- operadores
y) o pasarla como un ref out parámetro o.
Una fixed_pointer_initializer puede ser una de las siguientes:
El token " & " seguido de un variable_reference (reglas precisas para determinar la asignación definitiva) a
una variable móvil (variables fijas y móviles) de un tipo no administrado T , siempre que el tipo T* se
pueda convertir implícitamente al tipo de puntero proporcionado en la fixed instrucción. En este caso, el
inicializador calcula la dirección de la variable especificada y se garantiza que la variable permanece en una
dirección fija mientras dure la fixed instrucción.
Una expresión de un array_type con elementos de un tipo no administrado T , siempre que el tipo T* se
pueda convertir implícitamente al tipo de puntero proporcionado en la fixed instrucción. En este caso, el
inicializador calcula la dirección del primer elemento de la matriz y se garantiza que toda la matriz
permanece en una dirección fija mientras dure la fixed instrucción. Si la expresión de matriz es null o si la
matriz tiene cero elementos, el inicializador calcula una dirección igual a cero.
Una expresión de tipo string , siempre que el tipo char* se pueda convertir implícitamente al tipo de
puntero proporcionado en la fixed instrucción. En este caso, el inicializador calcula la dirección del primer
carácter de la cadena y se garantiza que toda la cadena permanece en una dirección fija mientras dure la
fixed instrucción. El comportamiento de la fixed instrucción está definido por la implementación si la
expresión de cadena es NULL.
Simple_name o member_access que hace referencia a un miembro de búfer de tamaño fijo de una variable
móvil, siempre que el tipo del miembro de búfer de tamaño fijo se pueda convertir implícitamente al tipo de
puntero proporcionado en la fixed instrucción. En este caso, el inicializador calcula un puntero al primer
elemento del búfer de tamaño fijo (búferes de tamaño fijo en expresiones) y se garantiza que el búfer de
tamaño fijo permanece en una dirección fija mientras dure la fixed instrucción.
Para cada dirección calculada por una fixed_pointer_initializer la fixed instrucción garantiza que la variable a la
que hace referencia la dirección no esté sujeta a reubicación o eliminación por parte del recolector de elementos
no utilizados durante la ejecución de la fixed instrucción. Por ejemplo, si la dirección calculada por un
fixed_pointer_initializer hace referencia a un campo de un objeto o un elemento de una instancia de matriz, la
fixed instrucción garantiza que la instancia del objeto contenedor no se reubique o se deseche durante la
duración de la instrucción.
Es responsabilidad del programador garantizar que los punteros creados por fixed instrucciones no
sobreviven más allá de la ejecución de esas instrucciones. Por ejemplo, cuando se pasan punteros creados por
fixed instrucciones a las API externas, es responsabilidad del programador asegurarse de que las API no
conservan memoria de estos punteros.
Los objetos fijos pueden provocar la fragmentación del montón (porque no se pueden moverse). Por ese
motivo, los objetos deben corregirse solo cuando sea absolutamente necesario y solo para la cantidad de
tiempo más corta posible.
En el ejemplo
class Test
{
static int x;
int y;
muestra varios usos de la fixed instrucción. La primera instrucción corrige y obtiene la dirección de un campo
estático, la segunda instrucción corrige y obtiene la dirección de un campo de instancia, y la tercera instrucción
corrige y obtiene la dirección de un elemento de matriz. En cada caso, se habría producido un error al usar el &
operador normal, ya que todas las variables se clasifican como variables móviles.
La cuarta fixed instrucción del ejemplo anterior genera un resultado similar al tercero.
Este ejemplo de la fixed instrucción utiliza string :
class Test
{
static string name = "xx";
En un contexto no seguro, los elementos de las matrices unidimensionales se almacenan en el orden de índice
creciente, empezando por index 0 y terminando por index Length - 1 . En el caso de las matrices
multidimensionales, los elementos de matriz se almacenan de forma que los índices de la dimensión situada
más a la derecha aumenten primero, después la dimensión izquierda siguiente y así sucesivamente hacia la
izquierda. En una fixed instrucción que obtiene un puntero p a una instancia de matriz a , los valores de
puntero que van desde p a p + a.Length - 1 representan las direcciones de los elementos de la matriz. Del
mismo modo, las variables que van desde p[0] a p[a.Length - 1] representan los elementos reales de la
matriz. Dado el modo en que se almacenan las matrices, podemos tratar una matriz de cualquier dimensión
como si fuera lineal.
Por ejemplo:
using System;
class Test
{
static void Main() {
int[,,] a = new int[2,3,4];
unsafe {
fixed (int* p = a) {
for (int i = 0; i < a.Length; ++i) // treat as linear
p[i] = i;
}
}
class Test
{
unsafe static void Fill(int* p, int count, int value) {
for (; count != 0; count--) *p++ = value;
}
una fixed instrucción se usa para corregir una matriz, por lo que su dirección se puede pasar a un método que
toma un puntero.
En el ejemplo:
class Test
{
unsafe static void PutString(string s, char* buffer, int bufSize) {
int len = s.Length;
if (len > bufSize) len = bufSize;
for (int i = 0; i < len; i++) buffer[i] = s[i];
for (int i = len; i < bufSize; i++) buffer[i] = (char)0;
}
Font f;
una instrucción Fixed se usa para corregir un búfer de tamaño fijo de una estructura, por lo que su dirección se
puede usar como puntero.
Un char* valor generado al corregir una instancia de cadena siempre apunta a una cadena terminada en NULL.
Dentro de una instrucción Fixed que obtiene un puntero p a una instancia de cadena s , los valores de
puntero que van desde p a p + s.Length - 1 representan direcciones de los caracteres de la cadena y el valor
del puntero p + s.Length apunta siempre a un carácter nulo (el carácter con valor '\0' ).
La modificación de objetos de tipo administrado a través de punteros fijos puede tener como resultado un
comportamiento indefinido. Por ejemplo, dado que las cadenas son inmutables, es responsabilidad del
programador asegurarse de que los caracteres a los que hace referencia un puntero a una cadena fija no se
modifican.
La terminación automática de las cadenas es especialmente útil cuando se llama a las API externas que esperan
cadenas de "estilo C". Sin embargo, tenga en cuenta que se permite que una instancia de cadena contenga
caracteres nulos. Si estos caracteres nulos están presentes, la cadena aparecerá truncada cuando se trate como
terminada en NULL char* .
struct_member_declaration_unsafe
: fixed_size_buffer_declaration
;
fixed_size_buffer_declaration
: attributes? fixed_size_buffer_modifier* 'fixed' buffer_element_type fixed_size_buffer_declarator+ ';'
;
fixed_size_buffer_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'unsafe'
;
buffer_element_type
: type
;
fixed_size_buffer_declarator
: identifier '[' constant_expression ']'
;
Una declaración de búfer de tamaño fijo puede incluir un conjunto de atributos (atributos), un new modificador
(Modificadores), una combinación válida de los cuatro modificadores de acceso (parámetros de tipo y
restricciones) y un unsafe modificador (contextos no seguros). Los atributos y modificadores se aplican a todos
los miembros declarados por la declaración de búfer de tamaño fijo. Es un error que el mismo modificador
aparezca varias veces en una declaración de búfer de tamaño fijo.
No se permite una declaración de búfer de tamaño fijo para incluir el static modificador.
El tipo de elemento de búfer de una declaración de búfer de tamaño fijo especifica el tipo de elemento de los
búferes introducidos por la declaración. El tipo de elemento de búfer debe ser uno de los tipos predefinidos
sbyte ,, byte short , ushort , int , uint , long , ulong , char , float , double o bool .
El tipo de elemento de búfer va seguido de una lista de declaradores de búfer de tamaño fijo, cada uno de los
cuales incorpora un nuevo miembro. Un declarador de búfer de tamaño fijo consta de un identificador que
denomina al miembro, seguido de una expresión constante encerrada en los [ ] tokens y. La expresión
constante denota el número de elementos del miembro introducido por ese declarador de búfer de tamaño fijo.
El tipo de la expresión constante se debe poder convertir implícitamente al tipo int y el valor debe ser un
entero positivo distinto de cero.
Se garantiza que los elementos de un búfer de tamaño fijo se organizan secuencialmente en la memoria.
Una declaración de búfer de tamaño fijo que declara varios búferes de tamaño fijo es equivalente a varias
declaraciones de una única declaración de búfer de tamaño fijo con los mismos atributos y tipos de elemento.
Por ejemplo
unsafe struct A
{
public fixed int x[5], y[10], z[100];
}
es equivalente a
unsafe struct A
{
public fixed int x[5];
public fixed int y[10];
public fixed int z[100];
}
class Test
{
unsafe static void PutString(string s, char* buffer, int bufSize) {
int len = s.Length;
if (len > bufSize) len = bufSize;
for (int i = 0; i < len; i++) buffer[i] = s[i];
for (int i = len; i < bufSize; i++) buffer[i] = (char)0;
}
Asignación de pila
En un contexto no seguro, una declaración de variable local (declaraciones de variables locales) puede incluir un
inicializador de asignación de pila que asigna memoria de la pila de llamadas.
local_variable_initializer_unsafe
: stackalloc_initializer
;
stackalloc_initializer
: 'stackalloc' unmanaged_type '[' expression ']'
;
El unmanaged_type indica el tipo de los elementos que se almacenarán en la ubicación recién asignada y la
expresión indica el número de estos elementos. En conjunto, estos especifican el tamaño de asignación
requerido. Dado que el tamaño de una asignación de pila no puede ser negativo, se trata de un error en tiempo
de compilación para especificar el número de elementos como un constant_expression que se evalúa como un
valor negativo.
Un inicializador de asignación de pila del formulario stackalloc T[E] requiere T que sea un tipo no
administrado (tipos de puntero) y E que sea una expresión de tipo int . La construcción asigna
E * sizeof(T) bytes de la pila de llamadas y devuelve un puntero de tipo T* al bloque recién asignado. Si E
es un valor negativo, el comportamiento es indefinido. Si E es cero, no se realiza ninguna asignación y el
puntero devuelto está definido por la implementación. Si no hay suficiente memoria disponible para asignar un
bloque del tamaño determinado, System.StackOverflowException se produce una excepción.
El contenido de la memoria recién asignada está sin definir.
No se permiten inicializadores de asignación de pila en catch finally bloques o (la instrucción try).
No hay ninguna manera de liberar explícitamente la memoria asignada mediante stackalloc . Todos los
bloques de memoria asignados a la pila creados durante la ejecución de un miembro de función se descartan
automáticamente cuando se devuelve ese miembro de función. Esto corresponde a la alloca función, una
extensión que se encuentra normalmente en las implementaciones de C y C++.
En el ejemplo
using System;
class Test
{
static string IntToString(int value) {
int n = value >= 0? value: -value;
unsafe {
char* buffer = stackalloc char[16];
char* p = buffer + 16;
do {
*--p = (char)(n % 10 + '0');
n /= 10;
} while (n != 0);
if (value < 0) *--p = '-';
return new string(p, 0, (int)(buffer + 16 - p));
}
}
stackalloc se usa un inicializador en el IntToString método para asignar un búfer de 16 caracteres en la pila.
El búfer se descarta automáticamente cuando el método vuelve.
using System;
using System.Runtime.InteropServices;
// Copies count bytes from src to dst. The source and destination
// blocks are permitted to overlap.
public static void Copy(void* src, void* dst, int count)
{
byte* ps = (byte*)src;
byte* pd = (byte*)dst;
if (ps > pd)
{
for (; count != 0; count--) *pd++ = *ps++;
}
else if (ps < pd)
{
for (ps += count, pd += count; count != 0; count--) *--pd = *--ps;
}
}
[DllImport("kernel32")]
private static extern void* HeapAlloc(IntPtr hHeap, int flags, UIntPtr size);
[DllImport("kernel32")]
private static extern bool HeapFree(IntPtr hHeap, int flags, void* block);
[DllImport("kernel32")]
private static extern void* HeapReAlloc(IntPtr hHeap, int flags, void* block, UIntPtr size);
[DllImport("kernel32")]
private static extern UIntPtr HeapSize(IntPtr hHeap, int flags, void* block);
}
En el ejemplo se asignan 256 bytes de memoria mediante Memory.Alloc e inicializa el bloque de memoria con
valores que aumentan de 0 a 255. A continuación, asigna una matriz de bytes de elemento 256 y utiliza
Memory.Copy para copiar el contenido del bloque de memoria en la matriz de bytes. Por último, el bloque de
memoria se libera mediante Memory.Free y el contenido de la matriz de bytes se genera en la consola.
Comentarios de documentación
18/09/2021 • 35 minutes to read
C# proporciona un mecanismo para que los programadores documenten su código mediante una sintaxis de
comentario especial que contiene texto XML. En los archivos de código fuente, se pueden usar comentarios que
tengan un formato determinado para dirigir una herramienta a fin de generar XML a partir de los comentarios y
los elementos de código fuente, que preceden. Los comentarios que usan esta sintaxis se denominan
*comentarios de documentación _. Deben preceder inmediatamente a un tipo definido por el usuario (como
una clase, un delegado o una interfaz) o un miembro (por ejemplo, un campo, un evento, una propiedad o un
método). La herramienta de generación de XML se denomina generador de documentación. (Este generador
podría ser, pero no es necesario, el propio compilador de C#). La salida generada por el generador de
documentación se denomina archivo de documentación. Un archivo de documentación se usa como entrada
para el visor de documentación de _ * * *; herramienta diseñada para generar algún tipo de presentación visual
de información de tipos y su documentación asociada.
Esta especificación sugiere un conjunto de etiquetas para usar en los comentarios de documentación, pero no se
requiere el uso de estas etiquetas, y se pueden usar otras etiquetas si se desea, siempre que se sigan las reglas
de XML con el formato correcto.
Introducción
Los comentarios que tienen una forma especial se pueden usar para dirigir una herramienta a fin de generar
XML a partir de los comentarios y los elementos de código fuente, que preceden. Estos comentarios son
comentarios de una sola línea que comienzan con tres barras diagonales ( /// ) o comentarios delimitados que
empiezan con una barra diagonal y dos estrellas ( /** ). Deben preceder inmediatamente a un tipo definido
por el usuario (como una clase, un delegado o una interfaz) o un miembro (como un campo, un evento, una
propiedad o un método) que anotan. Las secciones de atributos (especificación de atributos) se consideran parte
de las declaraciones, por lo que los comentarios de documentación deben preceder a los atributos aplicados a
un tipo o miembro.
Sintaxis:
single_line_doc_comment
: '///' input_character*
;
delimited_doc_comment
: '/**' delimited_comment_section* asterisk+ '/'
;
En un single_line_doc_comment, si hay un carácter de espacio en blanco después de los caracteres de cada una
/// de las single_line_doc_comment s adyacentes al single_line_doc_comment actual, ese carácter de espacio
en blanco no se incluye en la salida XML.
En un comentario delimitado-doc-comment, si el primer carácter que no es un espacio en blanco de la segunda
línea es un asterisco y el mismo patrón de caracteres de espacio en blanco opcionales y se repite un carácter de
asterisco al principio de cada línea dentro del comentario Delimited-doc-, los caracteres del patrón repetido no
se incluyen en la salida XML. El patrón puede incluir caracteres de espacio en blanco después de, así como el
carácter de asterisco.
Ejemplo :
/// <summary>Class <c>Point</c> models a point in a two-dimensional
/// plane.</summary>
///
public class Point
{
/// <summary>method <c>draw</c> renders the point.</summary>
void draw() {...}
}
El texto incluido en los comentarios de documentación debe ser correcto de acuerdo con las reglas de XML (
https://www.w3.org/TR/REC-xml) . Si el código XML tiene un formato incorrecto, se genera una advertencia y el
archivo de documentación contendrá un comentario que indica que se ha detectado un error.
Aunque los desarrolladores pueden crear su propio conjunto de etiquetas, se define un conjunto recomendado
en las etiquetas recomendadas. Algunas de las etiquetas recomendadas tienen significados especiales:
La etiqueta <param> se usa para describir parámetros. Si se usa una etiqueta de este tipo, el generador de
documentación debe comprobar que el parámetro especificado existe y que todos los parámetros se
describen en los comentarios de documentación. Si se produce un error en esta comprobación, el generador
de documentación emite una advertencia.
El atributo cref se puede asociar a cualquier etiqueta para proporcionar una referencia a un elemento de
código. El generador de documentación debe comprobar que este elemento de código existe. Si se produce
un error en la comprobación, el generador de documentación emite una advertencia. Al buscar un nombre
descrito en un cref atributo, el generador de documentación debe respetar la visibilidad del espacio de
nombres según using las instrucciones que aparecen en el código fuente. En el caso de los elementos de
código que son genéricos, no se puede usar la sintaxis genérica normal (es decir, " List<T> ") porque genera
XML no válido. Se pueden usar llaves en lugar de corchetes (es decir, " List{T} ") o se puede usar la sintaxis
de escape XML (es decir, " List<T> ").
La <summary> etiqueta está pensada para que la use un visor de documentación para mostrar información
adicional sobre un tipo o miembro.
La <include> etiqueta incluye información de un archivo XML externo.
Observe detenidamente que el archivo de documentación no proporciona información completa sobre el tipo y
los miembros (por ejemplo, no contiene ninguna información de tipo). Para obtener esta información sobre un
tipo o miembro, el archivo de documentación debe utilizarse junto con la reflexión en el tipo o miembro real.
Etiquetas recomendadas
El generador de documentación debe aceptar y procesar cualquier etiqueta que sea válida de acuerdo con las
reglas de XML. Las siguientes etiquetas proporcionan funcionalidad de uso común en la documentación del
usuario. (Por supuesto, son posibles otras etiquetas).
TA G SEC C IÓ N P RO P Ó SITO
<c>
Esta etiqueta proporciona un mecanismo para indicar que un fragmento de texto dentro de una descripción
debe establecerse en una fuente especial, como la que se usa para un bloque de código. Para las líneas de
código real, use <code> ( <code> ).
Sintaxis:
<c>text</c>
Ejemplo :
/// <summary>Class <c>Point</c> models a point in a two-dimensional
/// plane.</summary>
<code>
Esta etiqueta se usa para establecer una o más líneas de código fuente o el resultado del programa en alguna
fuente especial. Para fragmentos de código pequeños en narrativa, use <c> ( <c> ).
Sintaxis:
Ejemplo :
<example>
Esta etiqueta permite el código de ejemplo dentro de un comentario, para especificar cómo se puede usar un
método u otro miembro de la biblioteca. Normalmente, también implicaría el uso de la etiqueta <code> (
<code> ).
Sintaxis:
<example>description</example>
Ejemplo :
Vea <code> ( <code> ) para obtener un ejemplo.
<exception>
Esta etiqueta proporciona una manera de documentar las excepciones que un método puede iniciar.
Sintaxis:
<exception cref="member">description</exception>
where
member es el nombre de un miembro. El generador de documentación comprueba que el miembro dado
existe y se convierte member en el nombre de elemento canónico en el archivo de documentación.
description es una descripción de las circunstancias en las que se produce la excepción.
Ejemplo :
<include>
Esta etiqueta permite incluir información de un documento XML que es externo al archivo de código fuente. El
archivo externo debe ser un documento XML con el formato correcto y se aplica una expresión XPath a ese
documento para especificar el XML de ese documento que se va a incluir. <include> A continuación, la etiqueta
se reemplaza con el XML seleccionado del documento externo.
Sintaxis:
where
filename es el nombre de archivo de un archivo XML externo. El nombre de archivo se interpreta en relación
con el archivo que contiene la etiqueta include.
xpath es una expresión XPath que selecciona parte de XML en el archivo XML externo.
Ejemplo :
Si el código fuente contiene una declaración como:
<?xml version="1.0"?>
<extradoc>
<class name="IntList">
<summary>
Contains a list of integers.
</summary>
</class>
<class name="StringList">
<summary>
Contains a list of integers.
</summary>
</class>
</extradoc>
a continuación, la misma documentación se genera como si el código fuente incluyera:
/// <summary>
/// Contains a list of integers.
/// </summary>
public class IntList { ... }
<list>
Esta etiqueta se usa para crear una lista o una tabla de elementos. Puede contener un <listheader> bloque para
definir la fila de encabezado de una tabla o de una lista de definiciones. (Al definir una tabla, solo term es
necesario proporcionar una entrada para en el encabezado).
Cada elemento de la lista se especifica con un bloque <item> . Al crear una lista de definiciones, term
description deben especificarse y. Sin embargo, para una tabla, una lista con viñetas o una lista numerada, solo
description es necesario especificar.
Sintaxis:
where
term es el término que se va a definir, cuya definición está en description .
description es un elemento de una lista con viñetas o numerada, o la definición de un objeto term .
Ejemplo :
<para>
Esta etiqueta se usa dentro de otras etiquetas, como <summary> ( <remarks> )o <returns> ( <returns> ), y
permite agregar la estructura al texto.
Sintaxis:
<para>content</para>
/// <summary>This is the entry point of the Point class testing program.
/// <para>This program tests each method and operator, and
/// is intended to be run after any non-trivial maintenance has
/// been performed on the Point class.</para></summary>
public static void Main() {
// ...
}
<param>
Esta etiqueta se usa para describir un parámetro para un método, un constructor o un indizador.
Sintaxis:
<param name="name">description</param>
where
name es el nombre del parámetro.
description es una descripción del parámetro.
Ejemplo :
<paramref>
Esta etiqueta se usa para indicar que una palabra es un parámetro. El archivo de documentación se puede
procesar para dar formato a este parámetro de alguna manera distinta.
Sintaxis:
<paramref name="name"/>
<permission>
<permission cref="member">description</permission>
where
member es el nombre de un miembro. El generador de documentación comprueba que el elemento de
código dado existe y traduce el miembro al nombre de elemento canónico en el archivo de documentación.
description es una descripción del acceso al miembro.
Ejemplo :
<remarks>
Esta etiqueta se usa para especificar información adicional sobre un tipo. (Usar <summary> ( <summary> ) para
describir el tipo en sí y los miembros de un tipo).
Sintaxis:
<remarks>description</remarks>
<returns>
<returns>description</returns>
<see>
Esta etiqueta permite especificar un vínculo dentro del texto. Use <seealso> ( <seealso> ) para indicar el texto
que va a aparecer en una sección Vea también.
Sintaxis:
<see cref="member"/>
<seealso>
Esta etiqueta permite generar una entrada para la sección Vea también. Use <see> ( <see> ) para especificar un
vínculo desde dentro del texto.
Sintaxis:
<seealso cref="member"/>
/// <summary>This method determines whether two Points have the same
/// location.</summary>
/// <seealso cref="operator=="/>
/// <seealso cref="operator!="/>
public override bool Equals(object o) {
// ...
}
<summary>
Esta etiqueta se puede utilizar para describir un tipo o un miembro de un tipo. Use <remarks> ( <remarks> )
para describir el propio tipo.
Sintaxis:
<summary>description</summary>
<value>
<value>property description</value>
<typeparam>
Esta etiqueta se usa para describir un parámetro de tipo genérico para una clase, una estructura, una interfaz, un
delegado o un método.
Sintaxis:
<typeparam name="name">description</typeparam>
<typeparamref>
Esta etiqueta se usa para indicar que una palabra es un parámetro de tipo. El archivo de documentación se
puede procesar para dar formato a este parámetro de tipo de una manera distinta.
Sintaxis:
<typeparamref name="name"/>
/// <summary>This method fetches data and returns a list of <typeparamref name="T"/>.</summary>
/// <param name="query">query to execute</param>
public List<T> FetchData<T>(string query) {
...
}
C A RÁ C T ER DESC RIP C IÓ N
E Evento
F Campo
N Espacio de nombres
C A RÁ C T ER DESC RIP C IÓ N
La segunda parte de la cadena es el nombre completo del elemento, que empieza en la raíz del espacio
de nombres. El nombre del elemento, sus tipos envolventes y su espacio de nombres se separan por
puntos. Si el nombre del propio elemento tiene puntos, se reemplazan por #(U+0023) caracteres. (Se
supone que ningún elemento tiene este carácter en su nombre).
En el caso de los métodos y propiedades con argumentos, la lista de argumentos sigue entre paréntesis.
Para aquellos que no tienen argumentos, se omiten los paréntesis. Los argumentos están separados por
comas. La codificación de cada argumento es la misma que la firma de la CLI, como se indica a
continuación:
Los argumentos se representan mediante el nombre de la documentación, que se basa en su nombre
completo, modificado de la siguiente manera:
Los argumentos que representan tipos genéricos tienen un ` carácter anexado (acento grave)
seguido por el número de parámetros de tipo
Los argumentos que tienen el out ref modificador o tienen un @ nombre de tipo siguiente.
Los argumentos que se pasan por valor o por Via params no tienen ninguna notación especial.
Los argumentos que son matrices se representan como
[lowerbound:size, ... , lowerbound:size] donde el número de comas es el rango menos uno,
y los límites inferiores y el tamaño de cada dimensión, si se conocen, se representan en
formato decimal. Si no se especifica un límite inferior o un tamaño, se omite. Si se omiten el
límite inferior y el tamaño de una dimensión determinada, : también se omite. Las matrices
escalonadas se representan en una [] por nivel.
Los argumentos que tienen tipos de puntero distintos de void se representan mediante un *
siguiente nombre de tipo. Un puntero void se representa utilizando un nombre de tipo de
System.Void .
Los argumentos que hacen referencia a los parámetros de tipo genérico definidos en los tipos
se codifican mediante el ` carácter (acento grave) seguido por el índice de base cero del
parámetro de tipo.
Los argumentos que utilizan parámetros de tipo genérico definidos en métodos utilizan un
acento doble `` en lugar del ` utilizado para los tipos.
Los argumentos que hacen referencia a tipos genéricos construidos se codifican utilizando el
tipo genérico, seguido de { , seguido de una lista separada por comas de argumentos de tipo,
seguida de } .
Ejemplos de cadenas de ID.
Los ejemplos siguientes muestran un fragmento de código de C#, junto con la cadena de identificador generada
a partir de cada elemento de origen capaz de tener un Comentario de documentación:
Los tipos se representan mediante su nombre completo, ampliados con información genérica:
enum Color { Red, Blue, Green }
namespace Acme
{
interface IProcess {...}
class MyList<T>
{
class Helper<U,V> {...}
}
}
"T:Color"
"T:Acme.IProcess"
"T:Acme.ValueType"
"T:Acme.Widget"
"T:Acme.Widget.NestedClass"
"T:Acme.Widget.IMenuItem"
"T:Acme.Widget.Del"
"T:Acme.Widget.Direction"
"T:Acme.MyList`1"
"T:Acme.MyList`1.Helper`2"
"F:Acme.ValueType.total"
"F:Acme.Widget.NestedClass.value"
"F:Acme.Widget.message"
"F:Acme.Widget.defaultColor"
"F:Acme.Widget.PI"
"F:Acme.Widget.monthlyAverage"
"F:Acme.Widget.array1"
"F:Acme.Widget.array2"
"F:Acme.Widget.pCount"
"F:Acme.Widget.ppValues"
Constructores.
namespace Acme
{
class Widget: IProcess
{
static Widget() {...}
public Widget() {...}
public Widget(string s) {...}
}
}
"M:Acme.Widget.#cctor"
"M:Acme.Widget.#ctor"
"M:Acme.Widget.#ctor(System.String)"
Destructores.
namespace Acme
{
class Widget: IProcess
{
~Widget() {...}
}
}
"M:Acme.Widget.Finalize"
Modalidades.
namespace Acme
{
struct ValueType
{
public void M(int i) {...}
}
class MyList<T>
{
public void Test(T t) { }
}
class UseList
{
public void Process(MyList<int> list) { }
public MyList<T> GetValues<T>(T inputValue) { return null; }
}
}
"M:Acme.ValueType.M(System.Int32)"
"M:Acme.Widget.NestedClass.M(System.Int32)"
"M:Acme.Widget.M0"
"M:Acme.Widget.M1(System.Char,System.Single@,Acme.ValueType@)"
"M:Acme.Widget.M2(System.Int16[],System.Int32[0:,0:],System.Int64[][])"
"M:Acme.Widget.M3(System.Int64[][],Acme.Widget[0:,0:,0:][])"
"M:Acme.Widget.M4(System.Char*,Color**)"
"M:Acme.Widget.M5(System.Void*,System.Double*[0:,0:][])"
"M:Acme.Widget.M6(System.Int32,System.Object[])"
"M:Acme.MyList`1.Test(`0)"
"M:Acme.UseList.Process(Acme.MyList{System.Int32})"
"M:Acme.UseList.GetValues``(``0)"
Propiedades e indizadores.
namespace Acme
{
class Widget: IProcess
{
public int Width { get {...} set {...} }
public int this[int i] { get {...} set {...} }
public int this[string s, int i] { get {...} set {...} }
}
}
"P:Acme.Widget.Width"
"P:Acme.Widget.Item(System.Int32)"
"P:Acme.Widget.Item(System.String,System.Int32)"
Ceso.
namespace Acme
{
class Widget: IProcess
{
public event Del AnEvent;
}
}
"E:Acme.Widget.AnEvent"
Operadores unarios.
namespace Acme
{
class Widget: IProcess
{
public static Widget operator+(Widget x) {...}
}
}
"M:Acme.Widget.op_UnaryPlus(Acme.Widget)"
El conjunto completo de nombres de función de operador unario se usa como se indica a continuación:
op_UnaryPlus ,, op_UnaryNegation op_LogicalNot , op_OnesComplement , op_Increment , op_Decrement ,
op_True y op_False .
Operadores binarios.
namespace Acme
{
class Widget: IProcess
{
public static Widget operator+(Widget x1, Widget x2) {...}
}
}
"M:Acme.Widget.op_Addition(Acme.Widget,Acme.Widget)"
namespace Acme
{
class Widget: IProcess
{
public static explicit operator int(Widget x) {...}
public static implicit operator long(Widget x) {...}
}
}
"M:Acme.Widget.op_Explicit(Acme.Widget)~System.Int32"
"M:Acme.Widget.op_Implicit(Acme.Widget)~System.Int64"
Un ejemplo
Código fuente de C#
En el ejemplo siguiente se muestra el código fuente de una Point clase:
namespace Graphics
{
/// <summary>This method determines whether two Points have the same
/// location.</summary>
/// <param><c>o</c> is the object to be compared to the current object.
/// </param>
/// <returns>True if the Points have the same location and they have
/// the exact same type; otherwise, false.</returns>
/// <seealso cref="operator=="/>
/// <seealso cref="operator!="/>
public override bool Equals(object o) {
if (o == null) {
return false;
}
if (this == o) {
return true;
}
if (GetType() == o.GetType()) {
Point p = (Point)o;
return (X == p.X) && (Y == p.Y);
}
return false;
}
/// <summary>This operator determines whether two Points have the same
/// location.</summary>
/// <param><c>p1</c> is the first Point to be compared.</param>
/// <param><c>p2</c> is the second Point to be compared.</param>
/// <returns>True if the Points have the same location and they have
/// the exact same type; otherwise, false.</returns>
/// <seealso cref="Equals"/>
/// <seealso cref="operator!="/>
public static bool operator==(Point p1, Point p2) {
if ((object)p1 == null || (object)p2 == null) {
return false;
}
if (p1.GetType() == p2.GetType()) {
return (p1.X == p2.X) && (p1.Y == p2.Y);
}
return false;
}
/// <summary>This operator determines whether two Points have the same
/// location.</summary>
/// <param><c>p1</c> is the first Point to be compared.</param>
/// <param><c>p2</c> is the second Point to be compared.</param>
/// <returns>True if the Points do not have the same location and the
/// exact same type; otherwise, false.</returns>
/// <seealso cref="Equals"/>
/// <seealso cref="operator=="/>
public static bool operator!=(Point p1, Point p2) {
return !(p1 == p2);
}
XML resultante
A continuación se muestra la salida generada por un generador de documentación cuando se proporciona el
código fuente de la clase Point , mostrado anteriormente:
<?xml version="1.0"?>
<doc>
<assembly>
<name>Point</name>
</assembly>
<members>
<member name="T:Graphics.Point">
<summary>Class <c>Point</c> models a point in a two-dimensional
plane.
</summary>
</member>
<member name="F:Graphics.Point.x">
<summary>Instance variable <c>x</c> represents the point's
x-coordinate.</summary>
</member>
<member name="F:Graphics.Point.y">
<summary>Instance variable <c>y</c> represents the point's
y-coordinate.</summary>
</member>
<member name="M:Graphics.Point.#ctor">
<summary>This constructor initializes the new Point to
(0,0).</summary>
</member>
<member name="M:Graphics.Point.#ctor(System.Int32,System.Int32)">
<summary>This constructor initializes the new Point to
(<paramref name="xor"/>,<paramref name="yor"/>).</summary>
<param><c>xor</c> is the new Point's x-coordinate.</param>
<param><c>yor</c> is the new Point's y-coordinate.</param>
</member>
<member name="M:Graphics.Point.Move(System.Int32,System.Int32)">
<summary>This method changes the point's location to
the given coordinates.</summary>
<param><c>xor</c> is the new x-coordinate.</param>
<param><c>yor</c> is the new y-coordinate.</param>
<see cref="M:Graphics.Point.Translate(System.Int32,System.Int32)"/>
</member>
<member
name="M:Graphics.Point.Translate(System.Int32,System.Int32)">
<summary>This method changes the point's location by
the given x- and y-offsets.
<example>For example:
<code>
Point p = new Point(3,5);
p.Translate(-1,3);
</code>
results in <c>p</c>'s having the value (2,8).
</example>
</summary>
<param><c>xor</c> is the relative x-offset.</param>
<param><c>yor</c> is the relative y-offset.</param>
<see cref="M:Graphics.Point.Move(System.Int32,System.Int32)"/>
</member>
<member name="M:Graphics.Point.Equals(System.Object)">
<summary>This method determines whether two Points have the same
location.</summary>
<param><c>o</c> is the object to be compared to the current
object.
</param>
<returns>True if the Points have the same location and they have
the exact same type; otherwise, false.</returns>
<seealso
cref="M:Graphics.Point.op_Equality(Graphics.Point,Graphics.Point)"/>
<seealso
cref="M:Graphics.Point.op_Inequality(Graphics.Point,Graphics.Point)"/>
</member>
<member name="M:Graphics.Point.ToString">
<summary>Report a point's location as a string.</summary>
<returns>A string representing a point's location, in the form
(x,y),
without any leading, training, or embedded whitespace.</returns>
</member>
<member
name="M:Graphics.Point.op_Equality(Graphics.Point,Graphics.Point)">
<summary>This operator determines whether two Points have the
same
location.</summary>
<param><c>p1</c> is the first Point to be compared.</param>
<param><c>p2</c> is the second Point to be compared.</param>
<returns>True if the Points have the same location and they have
the exact same type; otherwise, false.</returns>
<seealso cref="M:Graphics.Point.Equals(System.Object)"/>
<seealso
cref="M:Graphics.Point.op_Inequality(Graphics.Point,Graphics.Point)"/>
</member>
<member
name="M:Graphics.Point.op_Inequality(Graphics.Point,Graphics.Point)">
<summary>This operator determines whether two Points have the
same
location.</summary>
<param><c>p1</c> is the first Point to be compared.</param>
<param><c>p1</c> is the first Point to be compared.</param>
<param><c>p2</c> is the second Point to be compared.</param>
<returns>True if the Points do not have the same location and
the
exact same type; otherwise, false.</returns>
<seealso cref="M:Graphics.Point.Equals(System.Object)"/>
<seealso
cref="M:Graphics.Point.op_Equality(Graphics.Point,Graphics.Point)"/>
</member>
<member name="M:Graphics.Point.Main">
<summary>This is the entry point of the Point class testing
program.
<para>This program tests each method and operator, and
is intended to be run after any non-trivial maintenance has
been performed on the Point class.</para></summary>
</member>
<member name="P:Graphics.Point.X">
<value>Property <c>X</c> represents the point's
x-coordinate.</value>
</member>
<member name="P:Graphics.Point.Y">
<value>Property <c>Y</c> represents the point's
y-coordinate.</value>
</member>
</members>
</doc>
Coincidencia de patrones para C# 7
18/09/2021 • 30 minutes to read
Las extensiones de coincidencia de patrones para C# permiten muchas de las ventajas de los tipos de datos
algebraicos y la coincidencia de patrones de los lenguajes funcionales, pero de una manera que se integra sin
problemas con la sensación del lenguaje subyacente. Las características básicas son: tipos de registros, que son
tipos cuyo significado semántico se describe mediante la forma de los datos. y la coincidencia de patrones, que
es un nuevo formulario de expresión que permite la descomposición de varios niveles de estos tipos de datos.
Los elementos de este enfoque están inspirados en las características relacionadas en los lenguajes de
programación F # y Scala.
Expresión is
El is operador se extiende para probar una expresión con un patrón.
relational_expression
: relational_expression 'is' pattern
;
Esta forma de relational_expression se suma a los formularios existentes en la especificación de C#. Es un error
en tiempo de compilación si el relational_expression a la izquierda del is token no designa un valor o no tiene
un tipo.
Cada identificador del patrón introduce una nueva variable local que se asigna definitivamente después de que
el is operador sea true (es decir, se asigna definitivamente cuando es true).
Nota: técnicamente existe una ambigüedad entre el tipo de una is-expression constant_pattern y,
cualquiera de los cuales puede ser un análisis válido de un identificador calificado. Intentamos enlazarlo
como un tipo para la compatibilidad con versiones anteriores del lenguaje; solo si se produce un error, se
resuelve como hacemos en otros contextos, lo primero que se encontró (que debe ser una constante o un
tipo). Esta ambigüedad solo está presente en la parte derecha de una is expresión.
Patrones
Los patrones se usan en el is operador y en una switch_statement para expresar la forma de los datos con los
que se van a comparar los datos entrantes. Los patrones pueden ser recursivos para que se puedan comparar
partes de los datos con subpatrones.
pattern
: declaration_pattern
| constant_pattern
| var_pattern
;
declaration_pattern
: type simple_designation
;
constant_pattern
: shift_expression
;
var_pattern
: 'var' simple_designation
;
Nota: técnicamente existe una ambigüedad entre el tipo de una is-expression constant_pattern y,
cualquiera de los cuales puede ser un análisis válido de un identificador calificado. Intentamos enlazarlo
como un tipo para la compatibilidad con versiones anteriores del lenguaje; solo si se produce un error, se
resuelve como hacemos en otros contextos, lo primero que se encontró (que debe ser una constante o un
tipo). Esta ambigüedad solo está presente en la parte derecha de una is expresión.
Modelo de declaración
El declaration_pattern ambos comprueba que una expresión es de un tipo determinado y la convierte a ese tipo
si la prueba se realiza correctamente. Si el simple_designation es un identificador, introduce una variable local
del tipo especificado denominado por el identificador especificado. Esa variable local se asigna definitivamente
cuando el resultado de la operación de coincidencia de patrones es true.
declaration_pattern
: type simple_designation
;
La semántica en tiempo de ejecución de esta expresión es que prueba el tipo en tiempo de ejecución del
operando del lado izquierdo relational_expression en el tipo del patrón. Si es de ese tipo en tiempo de ejecución
(o algún subtipo), el resultado de is operator es true . Declara una nueva variable local denominada por el
identificador al que se asigna el valor del operando izquierdo cuando el resultado es true .
Ciertas combinaciones de tipo estático del lado izquierdo y del tipo especificado se consideran incompatibles y
producen un error en tiempo de compilación. Se dice que un valor de tipo estático E es compatible con el tipo
T si existe una conversión de identidad, una conversión de referencia implícita, una conversión boxing, una
conversión de referencia explícita o una conversión unboxing de E a T . Es un error en tiempo de compilación
si una expresión de tipo E no es compatible con el tipo en un patrón de tipo con el que se encuentra una
coincidencia.
Nota: en C# 7,1 se amplía esto para permitir una operación de coincidencia de patrones si el tipo de entrada
o el tipo T es un tipo abierto. Este párrafo se sustituye por lo siguiente:
Ciertas combinaciones de tipo estático del lado izquierdo y del tipo especificado se consideran
incompatibles y producen un error en tiempo de compilación. Se dice que un valor de tipo estático E es
compatible con el tipo T si existe una conversión de identidad, una conversión de referencia implícita, una
conversión boxing, una conversión de referencia explícita o una conversión unboxing de E a T , o bien, si
E o T es un tipo abier to . Es un error en tiempo de compilación si una expresión de tipo E no es
compatible con el tipo en un patrón de tipo con el que se encuentra una coincidencia.
El modelo de declaración es útil para realizar pruebas de tipo en tiempo de ejecución de tipos de referencia y
reemplaza la expresión
int? x = 3;
if (x is int v) { // code using v }
constant_pattern
: shift_expression
;
Un patrón de constante prueba el valor de una expresión con respecto a un valor constante. La constante puede
ser cualquier expresión constante, como un literal, el nombre de una variable declarada const , una constante
de enumeración o una typeof expresión.
Si tanto e como c son de tipos enteros, se considera que el patrón coincide si el resultado de la expresión
e == c es true .
De lo contrario, el patrón se considera coincidente si object.Equals(e, c) devuelve true . En este caso, se trata
de un error en tiempo de compilación si el tipo estático de e no es compatible con el tipo de la constante.
Patrón var
var_pattern
: 'var' simple_designation
;
Una expresión e coincide con un var_pattern siempre. En otras palabras, una coincidencia con un patrón var
siempre se realiza correctamente. Si el simple_designation es un identificador, en tiempo de ejecución el valor de
e se enlaza a una variable local recién introducida. El tipo de la variable local es el tipo estático de e.
Es un error si el nombre se var enlaza a un tipo.
Instrucción switch
La switch instrucción se extiende para seleccionar la ejecución del primer bloque que tiene un patrón asociado
que coincide con la expresión switch.
switch_label
: 'case' complex_pattern case_guard? ':'
| 'case' constant_expression case_guard? ':'
| 'default' ':'
;
case_guard
: 'when' expression
;
No se define el orden en el que se buscan coincidencias con los patrones. Se permite que un compilador
coincida con los patrones desordenados y reutilizar los resultados de los patrones ya coincidentes para calcular
el resultado de la coincidencia de otros patrones.
Si hay una protección de casos , su expresión es de tipo bool . Se evalúa como una condición adicional que se
debe satisfacer para que el caso se considere satisfecho.
Es un error si un switch_label puede no tener ningún efecto en el tiempo de ejecución porque su patrón está en
los casos anteriores. [TODO: deberíamos ser más precisos sobre las técnicas que el compilador necesita usar
para alcanzar esta resolución.]
Una variable de patrón declarada en un switch_label se asigna definitivamente en su bloque case si y solo si ese
bloque Case contiene exactamente un switch_label.
[TODO: deberíamos especificar Cuándo se puede obtener acceso a un bloque switch .]
Ámbito de las variables de patrón
El ámbito de una variable declarada en un patrón es el siguiente:
Si el patrón es una etiqueta de caso, el ámbito de la variable es el bloque de casos.
En caso contrario, la variable se declara en una expresión is_pattern y su ámbito se basa en la construcción que
incluye inmediatamente la expresión que contiene la expresión is_pattern de la siguiente manera:
Si la expresión se encuentra en una expresión lambda con cuerpo de expresión, su ámbito es el cuerpo de la
lambda.
Si la expresión se encuentra en un método o propiedad con forma de expresión, su ámbito es el cuerpo del
método o propiedad.
Si la expresión está en una when cláusula de una catch cláusula, su ámbito es esa catch cláusula.
Si la expresión se encuentra en un iteration_statement, su ámbito es simplemente esa instrucción.
De lo contrario, si la expresión está en otra forma de instrucción, su ámbito es el ámbito que contiene la
instrucción.
Con el fin de determinar el ámbito, se considera que una embedded_statement está en su propio ámbito. Por
ejemplo, la gramática de una if_statement es
if_statement
: 'if' '(' boolean_expression ')' embedded_statement
| 'if' '(' boolean_expression ')' embedded_statement 'else' embedded_statement
;
Por tanto, si la instrucción controlada de un if_statement declara una variable de patrón, su ámbito está
restringido a ese embedded_statement:
En C# 7,3 hemos agregado los siguientes contextos en los que se puede declarar una variable de patrón:
Si la expresión se encuentra en un inicializador de constructor, su ámbito es el inicializador del
constructor y el cuerpo del constructor.
Si la expresión se encuentra en un inicializador de campo, su ámbito es el equals_value_clause en el que
aparece.
Si la expresión se encuentra en una cláusula de consulta que se especifica para traducirse en el cuerpo de
una expresión lambda, su ámbito es simplemente esa expresión.
F(G<A,B>(7));
podría interpretarse como una llamada a F con dos argumentos, G < A y B > (7) . Como alternativa,
podría interpretarse como una llamada a F con un argumento, que es una llamada a un método genérico
G con dos argumentos de tipo y un argumento normal.
Si se puede analizar una secuencia de tokens (en contexto) como un nombre simple (§ 7.6.3), acceso a
miembro (§ 7.6.5) o acceso a miembro de puntero (§ 18.5.2) que termina con una lista de argumentos de
tipo (§ 4.4.1), se examina el token inmediatamente posterior al token de cierre > . Si es uno de
( ) ] } : ; , . ? == != | ^
a continuación, la lista de argumentos de tipo se conserva como parte del acceso simple, de acceso a
miembros o de miembro de puntero , y se descarta cualquier otro posible análisis de la secuencia de tokens.
De lo contrario, la lista de argumentos de tipo no se considera parte del nombre simple, acceso a miembros
o > puntero- miembro-acceso, incluso si no hay otro posible análisis de la secuencia de tokens. Tenga en
cuenta que estas reglas no se aplican al analizar una lista de argumentos de tipo en un nombre de espacio
de nombres o-tipo (§ 3,8). La instrucción
F(G<A,B>(7));
, según esta regla, se interpretará como una llamada a F con un argumento, que es una llamada a un
método genérico G con dos argumentos de tipo y un argumento normal. Las instrucciones
cada uno de ellos se interpretará como una llamada a F con dos argumentos. La instrucción
x = F < A > +y;
se interpretará como un operador menor que, mayor que y unario más, como si se hubiera escrito la
instrucción x = (F < A) > (+y) , en lugar de un nombre simple con una lista de argumentos de tipo
seguido de un operador binario Plus. En la instrucción
x = y is C<T> + z;
los tokens C<T> se interpretan como un espacio de nombres o un nombre de tipo con una lista de
argumentos de tipo.
Hay una serie de cambios que se introducen en C# 7 que hacen que estas reglas de desambiguación dejen de
ser suficientes para controlar la complejidad del lenguaje.
Declaraciones de variable out
Ahora es posible declarar una variable en un argumento out:
Dado que la gramática de idioma para el argumento usa Expression, este contexto está sujeto a la regla de
desambiguación. En este caso, el cierre va > seguido de un identificador, que no es uno de los tokens que
permite que se trate como una lista de argumentos de tipo. Por lo tanto, se propone Agregar el identificador
al conjunto de tokens que desencadena la desambiguación en una lista de argumentos de tipo .
Tuplas y declaraciones de desconstrucción
Un literal de tupla se ejecuta exactamente en el mismo problema. Considere la expresión de tupla.
En las reglas anteriores de C# 6 para analizar una lista de argumentos, esto se analizaría como una tupla con
cuatro elementos, empezando por A < B como primer. Sin embargo, cuando esto aparece a la izquierda de una
desconstrucción, queremos que la desambiguación se desencadene por el token del identificador como se
describió anteriormente:
(A<B,C> D, E<F,G> H) = e;
Se trata de una declaración de desconstrucción que declara dos variables, la primera de las cuales es de tipo
A<B,C> y con nombre D . En otras palabras, el literal de tupla contiene dos expresiones, cada una de las cuales
es una expresión de declaración.
Para simplificar la especificación y el compilador, propongo que este literal de tupla se analice como una tupla
de dos elementos dondequiera que aparezca (ya sea o no aparece en el lado izquierdo de una asignación). Esto
sería un resultado natural de la desambiguación descrita en la sección anterior.
Coincidencia de patrones
La coincidencia de patrones introduce un nuevo contexto en el que se produce la ambigüedad del tipo de
expresión. Anteriormente, el lado derecho de un is operador era un tipo. Ahora puede ser un tipo o una
expresión, y si es un tipo, puede ir seguido de un identificador. Técnicamente, esto puede cambiar el significado
del código existente:
pero bajo bajo las reglas de C# 7 (con la desambiguación propuesta anteriormente) se analizaría como
var x = e is T<A> B;
que declara una variable B de tipo T<A> . Afortunadamente, los compiladores nativo y Roslyn tienen un error
por el que dan un error de sintaxis en el código de C# 6. Por lo tanto, este cambio importante concreto no es un
problema.
La coincidencia de patrones introduce tokens adicionales que deben impulsar la resolución de ambigüedades al
seleccionar un tipo. Los siguientes ejemplos de código de C# 6 válido existente se interrumpirían sin reglas de
desambiguación adicionales:
( ) ] } : ; , . ? == != | ^
en
( ) ] } : ; , . ? == != | ^ && || & [
Y, en ciertos contextos, tratamos el identificador como un token de ambigüedad. Esos contextos son donde la
secuencia de tokens que se va a ambigüedadr está inmediatamente precedida por una de las palabras clave is
, case , o out , o cuando se analiza el primer elemento de un literal de tupla (en cuyo caso los tokens van
precedidos de ( o : y el identificador va seguido de , ) o un elemento subsiguiente de un literal de tupla.
Regla de desambiguación modificada
La regla de desambiguación revisada sería similar a la siguiente
Si se puede analizar una secuencia de tokens (en contexto) como un nombre simple (§ 7.6.3), el acceso a
miembros (§ 7.6.5) o el acceso de miembro de puntero (§ 18.5.2) que termina con una lista de argumentos
de tipo (§ 4.4.1), se examina el token inmediatamente posterior al token de cierre > para ver si es
Uno de ( ) ] } : ; , . ? == != | ^ && || & [ ;o
Uno de los operadores relacionales < > <= >= is as ; o
Una palabra clave de consulta contextual que aparece dentro de una expresión de consulta; de
En ciertos contextos, tratamos el identificador como un token de ambigüedad. Estos contextos son donde
la secuencia de tokens que se va a inaprovechar está inmediatamente precedida por una de las palabras
clave is , o case out , o cuando se analiza el primer elemento de un literal de tupla (en cuyo caso los
tokens van precedidos de ( o : y el identificador va seguido de un , ) o un elemento subsiguiente de
un literal de tupla.
Si el token siguiente se encuentra entre esta lista o un identificador en un contexto de este tipo, la lista de
argumentos de tipo se conserva como parte del nombre simple, acceso a miembros o puntero a miembro
de puntero , y se descarta cualquier otro posible análisis de la secuencia de tokens. De lo contrario, la lista de
argumentos de tipo no se considera parte del acceso simple, de acceso a miembros o de miembro de
puntero, incluso si no hay otro posible análisis de la secuencia de tokens. Tenga en cuenta que estas reglas
no se aplican al analizar una lista de argumentos de tipo en un nombre de espacio de nombres o-tipo (§ 3,8).
if (expr is Type v) {
// code using v
}
Simplificación aritmética
Supongamos que definimos un conjunto de tipos recursivos para representar expresiones (según una
propuesta independiente):
Ahora se puede definir una función para calcular el derivado (no reducido) de una expresión:
Expr Deriv(Expr e)
{
switch (e) {
case X(): return Const(1);
case Const(*): return Const(0);
case Add(var Left, var Right):
return Add(Deriv(Left), Deriv(Right));
case Mult(var Left, var Right):
return Add(Mult(Deriv(Left), Right), Mult(Left, Deriv(Right)));
case Neg(var Value):
return Neg(Deriv(Value));
}
}
Expr Simplify(Expr e)
{
switch (e) {
case Mult(Const(0), *): return Const(0);
case Mult(*, Const(0)): return Const(0);
case Mult(Const(1), var x): return Simplify(x);
case Mult(var x, Const(1)): return Simplify(x);
case Mult(Const(var l), Const(var r)): return Const(l*r);
case Add(Const(0), var x): return Simplify(x);
case Add(var x, Const(0)): return Simplify(x);
case Add(Const(var l), Const(var r)): return Const(l+r);
case Neg(Const(var k)): return Const(-k);
default: return e;
}
}
Funciones locales
18/09/2021 • 3 minutes to read
Se extiende C# para admitir la declaración de funciones en el ámbito de bloque. Las funciones locales pueden
utilizar (Capture) variables del ámbito de inclusión.
El compilador usa el análisis de flujo para detectar las variables que usa una función local antes de asignarle un
valor. Cada llamada de la función requiere que estas variables se asignen de forma definitiva. Del mismo modo,
el compilador determina qué variables se asignan definitivamente en la devolución. Estas variables se
consideran definitivamente asignadas después de invocar la función local.
Se puede llamar a las funciones locales desde un punto léxico antes de su definición. Las instrucciones de
declaración de funciones locales no provocan una advertencia cuando no se puede tener acceso a ellas.
TODO: escribir especificación
Gramática de sintaxis
Esta gramática se representa como una diferencia de la gramática de especificación actual.
declaration-statement
: local-variable-declaration ';'
| local-constant-declaration ';'
+ | local-function-declaration
;
+local-function-declaration
+ : local-function-header local-function-body
+ ;
+local-function-header
+ : local-function-modifiers? return-type identifier type-parameter-list?
+ ( formal-parameter-list? ) type-parameter-constraints-clauses
+ ;
+local-function-modifiers
+ : (async | unsafe)
+ ;
+local-function-body
+ : block
+ | arrow-expression-body
+ ;
Las funciones locales pueden usar variables definidas en el ámbito de inclusión. La implementación actual
requiere que todas las variables leídas dentro de una función local se asignen definitivamente, como si se
ejecutara la función local en su punto de definición. Además, la definición de función local se debe haber
"ejecutado" en cualquier punto de uso.
Después de experimentar con ese bit (por ejemplo, no es posible definir dos funciones locales recursivas
mutuamente), hemos revisado cómo deseamos que funcione la asignación definitiva. La revisión (aún no
implementada) es que todas las variables locales leídas en una función local deben asignarse definitivamente en
cada invocación de la función local. Eso es realmente más sutil que el sonido y hay una gran cantidad de trabajo
que queda para que funcione. Una vez que lo haya hecho, podrá trasladar las funciones locales al final de su
bloque de inclusión.
Las nuevas reglas de asignación definitiva son incompatibles con la deducción del tipo de valor devuelto de una
función local, por lo que es probable que se quite la compatibilidad para deducir el tipo de valor devuelto.
A menos que se convierta una función local en un delegado, la captura se realiza en marcos que son tipos de
valor. Esto significa que no obtiene ninguna presión de GC del uso de funciones locales con captura.
Reachability
Agregamos a la especificación
El cuerpo de una expresión lambda con cuerpo de instrucción o función local se considera alcanzable.
Declaraciones de variable out
18/09/2021 • 2 minutes to read
La característica de declaración de variable out permite declarar una variable en la ubicación que se pasa como
out argumento.
argument_value
: 'out' type identifier
| ...
;
Una variable declarada de esta manera se denomina una variable out. Puede usar la palabra clave contextual
var para el tipo de la variable. El ámbito será el mismo que para una variable de patrón introducida a través de
la coincidencia de patrones.
Según la especificación del lenguaje (sección acceso al elemento 7.6.7), la lista de argumentos de un acceso a
elementos (expresión de indización) no contiene argumentos Ref o out. Sin embargo, las permite el compilador
para varios escenarios, por ejemplo, los indizadores declarados en los metadatos que aceptan out .
Dentro del ámbito de una variable local introducida por un argument_value, es un error en tiempo de
compilación hacer referencia a esa variable local en una posición textual que precede a su declaración.
También es un error hacer referencia a una variable out de tipo implícito (§ 8.5.1) en la misma lista de
argumentos que contiene inmediatamente su declaración.
La resolución de sobrecarga se modifica de la siguiente manera:
Agregamos una nueva conversión:
Existe una conversión de la expresión de una declaración de variable out implícitamente a cada tipo.
Asimismo
La conversión de la expresión de una declaración de variable out implícitamente no se considera mejor que
cualquier otra conversión de la expresión.
El tipo de una variable de salida con tipo implícito es el tipo del parámetro correspondiente en la firma del
método seleccionado por la resolución de sobrecarga.
El nuevo nodo DeclarationExpressionSyntax de sintaxis se agrega para representar la declaración en un
argumento out var.
Expresión Throw
18/09/2021 • 2 minutes to read
throw_expression
: 'throw' null_coalescing_expression
;
null_coalescing_expression
: throw_expression
;
Hay una solicitud relativamente común para agregar literales binarios a C# y VB. En el caso de las máscaras de.
(por ejemplo, las enumeraciones de marcas) esto parece realmente útil, pero también sería excelente solo con
fines educativos.
Los literales binarios tendrían el siguiente aspecto:
Sintácticamente y semánticamente son idénticos a los literales hexadecimales, excepto en el caso de usar en b /
B lugar de x / X , tener solo dígitos 0 y 1 y interpretarse en base 2 en lugar de 16.
El costo de la implementación de estos y la poca sobrecarga conceptual para los usuarios del lenguaje es escaso.
Sintaxis
La gramática sería la siguiente:
integer-literal:
: ...
| binary-integer-literal
;
binary-integer-literal:
: `0b` binary-digits integer-type-suffix-opt
| `0B` binary-digits integer-type-suffix-opt
;
binary-digits:
: binary-digit
| binary-digits binary-digit
;
binary-digit:
: `0`
| `1`
;
Separadores de dígitos
18/09/2021 • 2 minutes to read
La posibilidad de agrupar los dígitos en literales numéricos de gran tamaño tendría un gran impacto en la
legibilidad y sin ningún inconveniente significativo.
Agregar literales binarios (#215) aumentaría la probabilidad de que los literales numéricos sean largos, por lo
que las dos características se mejoran.
Seguiremos Java y otros, y usaremos un carácter de subrayado _ como separador de dígitos. Podría
producirse en cualquier parte en un literal numérico (excepto en el primer y el último carácter), ya que las
agrupaciones diferentes pueden tener sentido en escenarios diferentes y especialmente en diferentes bases
numéricas:
Cualquier secuencia de dígitos puede estar separada por caracteres de subrayado, posiblemente más de un
carácter de subrayado entre dos dígitos consecutivos. Se permiten en los decimales y en los exponentes, pero
después de la regla anterior, es posible que no aparezcan junto a la coma decimal ( 10_.0 ), junto al carácter de
exponente ( 1.1e_1 ) o al lado del especificador de tipo ( 10_f ). Cuando se usa en literales binarios y
hexadecimales, puede que no aparezcan inmediatamente después de 0x o 0b .
La sintaxis es sencilla y los separadores no tienen ningún impacto semántico; simplemente se omiten.
Esto tiene un gran valor y es fácil de implementar.
Tipos de tareas asincrónicas en C #
18/09/2021 • 6 minutes to read
Extender async para admitir tipos de tarea que coinciden con un patrón específico, además de los tipos
conocidos System.Threading.Tasks.Task y System.Threading.Tasks.Task<T> .
Tipo de tarea
Un tipo de tarea es class o con un tipo de generador asociado identificado con
struct
System.Runtime.CompilerServices.AsyncMethodBuilderAttribute . El tipo de tarea puede no ser genérico, para los
métodos asincrónicos que no devuelven un valor, o genérico, para los métodos que devuelven un valor.
Para admitir await , el tipo de tarea debe tener un método accesible y correspondiente GetAwaiter() que
devuelva una instancia de un tipo de Await (consulte expresiones de espera admitidas de C# 7.7.7.1).
[AsyncMethodBuilder(typeof(MyTaskMethodBuilder<>))]
class MyTask<T>
{
public Awaiter<T> GetAwaiter();
}
Tipo de generador
El tipo de generador es class o, struct que corresponde al tipo de tarea específico. El tipo de generador
puede tener un parámetro de tipo como máximo y no debe estar anidado en un tipo genérico. El tipo de
generador tiene los public métodos siguientes. En el caso de los tipos de generador no genéricos, SetResult()
no tiene parámetros.
class MyTaskMethodBuilder<T>
{
public static MyTaskMethodBuilder<T> Create();
Ejecución
El compilador usa los tipos anteriores para generar el código para la máquina de Estados de un async método.
(El código generado es equivalente al código generado para los métodos asincrónicos que devuelven Task ,
Task<T> o void . La diferencia es que, para esos tipos conocidos, el compilador también conoce los tipos de
generador ).
Builder.Create() se invoca para crear una instancia del tipo de generador.
Si el equipo de estado se implementa como struct , builder.SetStateMachine(stateMachine) se llama a con una
instancia de conversión boxing de la máquina de Estados que el generador puede almacenar en caché si es
necesario.
builder.Start(ref stateMachine) se invoca para asociar el generador a la instancia de máquina de Estados
generada por el compilador. El generador debe llamar stateMachine.MoveNext() a en Start() o después de que
Start() haya devuelto para avanzar el equipo de estado. Después de Start() que devuelva, el async método
llama a builder.Task para que la tarea vuelva del método asincrónico.
Cada llamada a stateMachine.MoveNext() hará avanzar el equipo de estado. Si la máquina de Estados se
completa correctamente, builder.SetResult() se llama a, con el valor devuelto del método, si existe. Si se
produce una excepción en el equipo de estado, builder.SetException(exception) se llama a.
Si el equipo de estado alcanza una await expr expresión, expr.GetAwaiter() se invoca. Si el esperador
implementa ICriticalNotifyCompletion y IsCompleted es false, la máquina de Estados invoca
builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine) . AwaitUnsafeOnCompleted() debe llamar a
awaiter.OnCompleted(action) con una acción que llama a stateMachine.MoveNext() cuando se completa el Await.
Similar a INotifyCompletion y builder.AwaitOnCompleted() .
Overload Resolution
La resolución de sobrecarga se extiende para reconocer los tipos de tarea además de Task y Task<T> .
Una async expresión lambda sin ningún valor devuelto es una coincidencia exacta de un parámetro de
candidato de sobrecarga de tipo de tarea no genérico y una expresión async Lambda con el tipo de valor
devuelto T es una coincidencia exacta para un parámetro de candidato de sobrecarga del tipo de tarea
genérico.
De lo contrario, si una async expresión lambda no es una coincidencia exacta para ninguno de los dos
parámetros candidatos de tipos de tarea, o una coincidencia exacta para ambos, y hay una conversión implícita
de un tipo de candidato al otro, el de candidato de WINS. De lo contrario, evalúe de forma recursiva los tipos A
y B dentro de Task1<A> y Task2<B> para obtener una mejor coincidencia.
De lo contrario, si una async expresión lambda no es una coincidencia exacta para ninguno de los dos
parámetros candidatos de tipos de tarea, pero un candidato es un tipo más especializado que el otro, gana el
candidato más especializado.
Async principal
18/09/2021 • 4 minutes to read
[x] propuesto
[] Prototipo
[] Implementación
[] Especificación
Resumen
Permita await que se use en el método principal/EntryPoint de una aplicación permitiendo que el punto de
entrada devuelva Task / Task<int> y se marque async .
Motivación
Es muy común al aprender C#, al escribir utilidades basadas en consola y al escribir aplicaciones de prueba
pequeñas para que quieran llamar a await async métodos y desde Main. Hoy en día, agregamos un nivel de
complejidad al forzar que este await se realice en un método asincrónico independiente, lo que hace que los
desarrolladores tengan que escribir elementos reutilizables como el siguiente para empezar:
Podemos eliminar la necesidad de este texto reutilizable y facilitar la introducción, ya que permite que el propio
principal sea async tal que se await pueda usar en él.
Diseño detallado
Actualmente se permiten las siguientes firmas entryPoints:
Para evitar riesgos de compatibilidad, estas nuevas firmas solo se considerarán como entryPoints válidas si no
hay sobrecargas del conjunto anterior. El lenguaje/compilador no requerirá que el punto de entrada esté
marcado como async , aunque esperamos que la mayoría de los usos se marque como tal.
Cuando uno de ellos se identifica como el punto de entrada, el compilador sintetizará un método de punto de
entrada real que llama a uno de estos métodos codificados:
static Task Main() dará lugar a que el compilador emita el equivalente de
private static void $GeneratedMain() => Main().GetAwaiter().GetResult();
static Task Main(string[]) dará lugar a que el compilador emita el equivalente de
private static void $GeneratedMain(string[] args) => Main(args).GetAwaiter().GetResult();
static Task<int> Main() dará lugar a que el compilador emita el equivalente de
private static int $GeneratedMain() => Main().GetAwaiter().GetResult();
static Task<int> Main(string[]) dará lugar a que el compilador emita el equivalente de
private static int $GeneratedMain(string[] args) => Main(args).GetAwaiter().GetResult();
Ejemplo de uso:
using System;
using System.Net.Http;
class Test
{
static async Task Main(string[] args) =>
Console.WriteLine(await new HttpClient().GetStringAsync(args[0]));
}
Inconvenientes
La principal desventaja es simplemente la complejidad adicional de admitir firmas de punto de entrada
adicionales.
Alternativas
Otras variantes consideradas:
Permite async void . Necesitamos mantener la semántica igual para que el código la llame directamente, lo que
a continuación dificultaría que un punto de entrada generado lo llamara (no se devuelve ninguna tarea).
Podríamos resolver esto generando otros dos métodos, por ejemplo,
se convierte en
Preguntas no resueltas
N/D
Design Meetings
N/D
Literal "default" de tipo de destino
18/09/2021 • 3 minutes to read
[x] propuesto
[x] prototipo
[x] implementación
[] Especificación
Resumen
La característica con tipo de destino default es una variación de forma más corta del default(T) operador, lo
que permite omitir el tipo. En su lugar, se deduce su tipo mediante el establecimiento de destinos. Aparte de eso,
se comporta como default(T) .
Motivación
La principal motivación es evitar escribir información redundante.
Por ejemplo, al invocar void Method(ImmutableArray<SomeType> array) , el literal predeterminado permite
M(default) en lugar de M(default(ImmutableArray<SomeType>)) .
Diseño detallado
Se introduce una nueva expresión, el literal predeterminado . Una expresión con esta clasificación se puede
convertir implícitamente a cualquier tipo mediante una conversión literal predeterminada.
La inferencia del tipo del literal predeterminado funciona igual que para el literal null , salvo que se permite
cualquier tipo (no solo los tipos de referencia).
Esta conversión genera el valor predeterminado del tipo deducido.
El literal predeterminado puede tener un valor constante, dependiendo del tipo deducido. Por lo tanto
const int x = default; , es válido, pero const int? y = default; no lo es.
El literal predeterminado puede ser el operando de los operadores de igualdad, siempre que el otro operando
tenga un tipo. default == x Y x == default son expresiones válidas, pero default == default no es válido.
Inconvenientes
Una desventaja menor es que se puede usar el literal predeterminado en lugar de un literal nulo en la mayoría
de los contextos. Dos de las excepciones son throw null; y null == null , que se permiten para el literal null ,
pero no el literal predeterminado .
Alternativas
Hay un par de alternativas a tener en cuenta:
El estado quo: la característica no está justificada por sus propios méritos y los desarrolladores siguen
usando el operador predeterminado con un tipo explícito.
Extender el literal NULL: este es el enfoque de VB con Nothing . Podríamos permitir int x = null; .
Preguntas no resueltas
[x] ¿se debe permitir de forma predeterminada como operando de los operadores is o as ? Respuesta: no
permitir default is T , permitir x is default , permitir default as RefType (con ADVERTENCIA siempre
nula)
Design Meetings
LDM 3/7/2017
LDM 3/28/2017
LDM 5/31/2017
Inferir nombres de tupla (también conocidos como
inicializadores de proyección de tupla)
18/09/2021 • 4 minutes to read
Resumen
En una serie de casos comunes, esta característica permite omitir los nombres de elemento de tupla y, en su
lugar, deducirlos. Por ejemplo, en lugar de escribir (f1: x.f1, f2: x?.f2) , los nombres de elemento "F1" y "F2"
se pueden inferir de (x.f1, x?.f2) .
Esto es paralelo al comportamiento de los tipos anónimos, que permiten deducir los nombres de miembro
durante la creación. Por ejemplo, new { x.f1, y?.f2 } declara los miembros "F1" y "F2".
Esto es especialmente útil cuando se usan tuplas en LINQ:
Diseño detallado
Hay dos partes del cambio:
1. Intente inferir un nombre de candidato para cada elemento de tupla que no tenga un nombre explícito:
Usar las mismas reglas que la inferencia de nombres para los tipos anónimos.
En C#, esto permite tres casos: y (identifier), x.y (acceso a miembros simple) y x?.y (acceso
condicional).
En VB, esto permite casos adicionales, como x.y() .
Rechazar nombres de tupla reservados (distingue mayúsculas de minúsculas en C#, sin distinción de
mayúsculas y minúsculas en VB), ya que están prohibidos o ya están implícitos. Por ejemplo, como
ItemN , Rest y ToString .
Si los nombres de candidatos son duplicados (con distinción de mayúsculas y minúsculas en C#, sin
distinción de mayúsculas y minúsculas en VB) en toda la tupla, se quitan esos candidatos.
2. Durante las conversiones (que comprueban y avisan sobre la eliminación de nombres de los literales de
tupla), los nombres inferidos no generarán ninguna advertencia. Esto evita que se interrumpa el código de
tupla existente.
Tenga en cuenta que la regla para administrar duplicados es diferente de la de los tipos anónimos. Por ejemplo,
new { x.f1, x.f1 } genera un error, pero (x.f1, x.f1) todavía se permite (solo sin nombres inferidos). Esto
evita que se interrumpa el código de tupla existente.
Por coherencia, lo mismo se aplica a las tuplas generadas por las asignaciones de desconstrucción (en C#):
Lo mismo se aplica también a las tuplas de VB, mediante las reglas específicas de VB para deducir el nombre de
las comparaciones de expresiones y de nombres que no distinguen mayúsculas de minúsculas.
Al usar el compilador de C# 7,1 (o posterior) con la versión de idioma "7,0", los nombres de los elementos se
deducen (a pesar de que la característica no esté disponible), pero se producirá un error de uso del sitio para
intentar tener acceso a ellos. Esto limitará las adiciones de código nuevo que posteriormente se verán ante el
problema de compatibilidad (que se describe a continuación).
Inconvenientes
La principal desventaja es que esto presenta una interrupción de compatibilidad de C# 7,0:
El Consejo de compatibilidad encontró este salto aceptable, dado que es limitado y el período de tiempo desde
que las tuplas enviadas (en C# 7,0) es breve.
Referencias
LDM, 4 de abril de 2017
Debate en github (gracias @alrz por llevar este problema)
Diseño de tuplas
coincidencia de patrones con genéricos
18/09/2021 • 3 minutes to read
[x] propuesto
[] Prototipo:
[] Implementación:
[] Especificación:
Resumen
La especificación para el operador de C# as existente permite que no haya conversión entre el tipo del operando
y el tipo especificado cuando uno de los dos es un tipo abierto. Sin embargo, en C# 7, el Type identifier
patrón requiere que haya una conversión entre el tipo de la entrada y el tipo dado.
Se propone relajar este y cambiar expression is Type identifier , además de permitirse en las condiciones en
las que se permite en C# 7, para permitirse también cuando se expression as Type permita. En concreto, los
nuevos casos son casos en los que el tipo de la expresión o el tipo especificado es un tipo abierto.
Motivación
Los casos en los que la coincidencia de patrones debe permitirse "obviamente" no se pueden compilar
actualmente. Consulte, por ejemplo, https://github.com/dotnet/roslyn/issues/16195 .
Diseño detallado
Cambiamos el párrafo en la especificación de coincidencia de patrones (la suma propuesta se muestra en
negrita):
Ciertas combinaciones de tipo estático del lado izquierdo y del tipo especificado se consideran
incompatibles y producen un error en tiempo de compilación. Se dice que un valor de tipo estático E es
compatible con el tipo T si existe una conversión de identidad, una conversión de referencia implícita, una
conversión boxing, una conversión de referencia explícita o una conversión unboxing de E a T , o bien,
si E o T es un tipo abier to . Es un error en tiempo de compilación si una expresión de tipo E no es
compatible con el tipo en un patrón de tipo con el que se encuentra una coincidencia.
Inconvenientes
Ninguno.
Alternativas
Ninguno.
Preguntas no resueltas
Ninguno.
Design Meetings
LDM consideró esta pregunta y creía que era un cambio de nivel de corrección de errores. Lo tratamos como
una característica de lenguaje independiente porque la realización del cambio una vez que se ha lanzado el
idioma presentaría una incompatibilidad de avance. El uso del cambio propuesto requiere que el programador
especifique la versión de idioma 7,1.
Referencias readonly
18/09/2021 • 48 minutes to read
[x] propuesto
[x] prototipo
[x] implementación: iniciada
[] Especificación: no iniciada
Resumen
La característica "referencias de solo lectura" es realmente un grupo de características que aprovechan la eficacia
de pasar variables por referencia, pero sin exponer los datos a modificaciones:
in los
Devoluciones de ref readonly
readonly Structs
ref / in métodos de extensión
ref readonly variables locales
ref expresiones condicionales
in los parámetros se declaran mediante in la palabra clave como modificador en la Signatura del parámetro.
Para todos los propósitos in , el parámetro se trata como una readonly variable. La mayoría de las
restricciones sobre el uso de in parámetros dentro del método son las mismas que con readonly los campos.
Por ejemplo, los campos de un in parámetro que tiene un tipo de estructura se clasifican de forma recursiva
como readonly variables.
// not OK!!
v1.X = 0;
// not OK!!
foo(ref v1.X);
// OK
return new Vector3(v1.X + v2.X, v1.Y + v2.Y, v1.Z + v2.Z);
}
in los parámetros se permiten en cualquier lugar donde se permitan los parámetros de ByVal normales.
Esto incluye indizadores, operadores (incluidas las conversiones), delegados, expresiones lambda, funciones
locales.
in no se permite en combinación con out o con algo que out no se combina con.
No se permite la sobrecarga de ref / out / in diferencias.
Se permite sobrecargar en las diferencias y ByVal normales in .
Para el propósito de OHI (sobrecargar, ocultar, implementar), in se comporta de forma similar a un out
parámetro. Se aplican las mismas reglas. Por ejemplo, el método de reemplazo tendrá que coincidir in
con los parámetros con in los parámetros de un tipo de identidad que se pueda convertir.
Para el propósito de las conversiones de grupo de delegado/lambda/método, in se comporta de forma
similar a un out parámetro. Las expresiones lambda y los candidatos de conversión de grupos de
métodos aplicables tendrán que coincidir con los in parámetros del delegado de destino con in
parámetros de un tipo que se pueda convertir en identidades.
Para el propósito de la varianza genérica, in los parámetros son no variantes.
Nota: no hay advertencias en in los parámetros que tienen tipos de referencia o primitivos. Puede que no
sea puntual en general, pero en algunos casos el usuario debe/desea pasar primitivos como in . Ejemplos:
invalidar un método genérico como Method(in T param) cuando T se sustituyó por int o cuando tiene
métodos como Volatile.Read(in int location)
Es imaginable tener un analizador que advierte en los casos de uso ineficaz de in los parámetros, pero las
reglas para dicho análisis serían demasiado aproximadas para formar parte de una especificación de
lenguaje.
int x = 1;
void M1<T>(in T x)
{
// . . .
}
class D
{
public string this[in Guid index];
}
D dictionary = . . . ;
var y = dictionary[in Guid.Empty]; // in argument to an indexer
in el argumento debe ser un valor l (*) legible . Ejemplo: M1(in 42) no es válido
En concreto, es válido pasar readonly campos, in parámetros u otras variables formal readonly como
in argumentos. Ejemplo: dictionary[in Guid.Empty] es legal. Guid.Empty es un campo de solo lectura
estático.
in el argumento debe tener la identidad de tipo que se pueda convertir al tipo del parámetro. Ejemplo:
M1<object>(in Guid.Empty) no es válido. Guid.Empty no se pueden convertir las identidades en object
La motivación de las reglas anteriores es que los in argumentos garantizan el alias de la variable de
argumento. El destinatario siempre recibe una referencia directa a la misma ubicación que representa el
argumento.
en situaciones excepcionales en las que los in argumentos se deben desbordar por pila debido a que las
await expresiones se usan como operandos de la misma llamada, el comportamiento es el mismo que con
los out ref argumentos y. Si la variable no se puede desbordar de manera referencial, se muestra un error.
Ejemplos:
1. M1(in staticField, await SomethingAsync())es válido. staticField es un campo estático al que se puede
tener acceso más de una vez sin efectos secundarios observables. Por lo tanto, se pueden proporcionar el
orden de los efectos secundarios y los requisitos de alias.
2. M1(in RefReturningMethod(), await SomethingAsync()) producirá un error. RefReturningMethod() es un ref
método que se devuelve. Una llamada al método puede tener efectos secundarios observables, por lo que
debe evaluarse antes que el SomethingAsync() operando. Sin embargo, el resultado de la invocación es una
referencia que no se puede conservar en el await punto de suspensión, lo que hace imposible el requisito
de referencia directa.
void Print<T>(in T x)
{
//. . .
}
En ese caso, se pasa una referencia a un valor convertido temporal que contiene. Ejemplo:
ref / in En este documento se proporciona más información sobre los métodos de extensión.
el desbordamiento de argumentos debido a await operandos podría sobrepasar "por valor", si es necesario.
En los escenarios en los que no es posible proporcionar una referencia directa al argumento, en await su
lugar se vuelca una copia del valor del argumento.
Ejemplo:
M1(RefReturningMethod(), await SomethingAsync()) // not an error.
Dado que el resultado de una invocación de efectos secundarios es una referencia que no se puede conservar
en la await suspensión, se conservará en su lugar un temporal que contenga el valor real (tal como lo haría en
un caso de parámetro ByVal normal).
Argumentos opcionales omitidos
Se permite in que un parámetro especifique un valor predeterminado. Esto hace que el argumento
correspondiente sea opcional.
Si se omite el argumento opcional en el sitio de llamada, se pasa el valor predeterminado a través de un
temporal.
Motivación : esto se hace para asegurarse de que, en un caso del método de invalidación o implementación de
los in parámetros coinciden.
Los mismos requisitos se aplican a Invoke los métodos de los delegados.
Motivación : esto es para asegurarse de que los compiladores existentes no se pueden omitir simplemente
readonly al crear o asignar delegados.
ref readonly las devoluciones se permiten en los mismos lugares en los que ref se permiten las
devoluciones. Esto incluye indexadores, delegados, expresiones lambda y funciones locales.
No se permite sobrecargar las ref / ref readonly diferencias.
Se permite sobrecargar en valores de ByVal normales y ref readonly devolver diferencias.
Para el propósito de OHI (sobrecargar, ocultar, implementar), ref readonly es similar pero distinto de
ref . Por ejemplo, un método que invalida ref readonly uno, debe ser y tener un tipo que se pueda
ref readonly convertir en identidades.
Para el propósito de la varianza genérica, las ref readonly devoluciones son no variantes.
Nota: no hay advertencias en las ref readonly devoluciones que tienen tipos de referencia o primitivos.
Puede que no sea puntual en general, pero en algunos casos el usuario debe/desea pasar primitivos como
in . Ejemplos: invalidar un método genérico como ref readonly T Method() cuando T se sustituyó por
int .
Es posible tener un analizador que advierte en casos de uso ineficaz de ref readonly las devoluciones, pero
las reglas para dicho análisis serían demasiado aproximadas para formar parte de una especificación de
lenguaje.
La motivación es que return ref readonly <expression> es una duración innecesaria y solo permite
discrepancias en la readonly parte que siempre darían lugar a errores. ref Sin embargo, es necesario para la
coherencia con otros escenarios en los que se pasa algo a través de un alias estricto frente a un valor.
A diferencia del caso con in parámetros, ref readonly devuelve nunca devolver a través de una copia
local. Es posible que la copia deje de existir inmediatamente después de que se devuelva este procedimiento.
Por consiguiente, ref readonly las devoluciones son siempre referencias directas.
Ejemplo:
struct ImmutableArray<T>
{
private readonly T[] array;
Tenga en cuenta la situación en la que se pasa un valor r a un in parámetro a través de una copia y
después se devuelve en un formulario de ref readonly . En el contexto del llamador, el resultado de dicha
invocación es una referencia a los datos locales y, como tal, no es seguro devolver. Una vez que RValues no
es seguro devolver, la regla existente #6 ya controla este caso.
Ejemplo:
Nota: existen otras reglas relacionadas con la seguridad de las devoluciones que entran en juego cuando se
implican tipos de referencia y reasignaciones de referencia. Las reglas se aplican igualmente a ref
ref readonly los miembros y y, por lo tanto, no se mencionan aquí.
Comportamiento de alias.
ref readonly los miembros proporcionan el mismo comportamiento de alias que ref los miembros
ordinarios (excepto para ser de solo lectura). Por lo tanto, para la captura en lambdas, Async, iteradores,
desbordamiento de pila, etc. se aplican las mismas restricciones. es decir,. debido a la incapacidad de capturar las
referencias reales y debido a la naturaleza de efectos secundarios de la evaluación de miembros, tales
escenarios no se permiten.
Se permite y es necesario realizar una copia cuando el ref readonly valor devuelto es un receptor de
métodos struct normales, que toman this como referencia ordinariamente modificable. Históricamente, en
todos los casos en los que estas invocaciones se aplican a la variable de solo lectura, se realiza una copia
local.
Representación de metadatos.
Cuando System.Runtime.CompilerServices.IsReadOnlyAttribute se aplica a la devolución de un método que
devuelve ByRef, significa que el método devuelve una referencia de solo lectura.
Además, la firma de resultados de estos métodos (y solo esos métodos) debe tener
modreq[System.Runtime.CompilerServices.IsReadOnlyAttribute] .
Motivación : esto es para asegurarse de que los compiladores existentes no se pueden omitir simplemente
readonly al invocar métodos con ref readonly Returns.
Estructuras readonly
En Resumen, es una característica que convierte el this parámetro de todos los miembros de instancia de un
struct, excepto para los constructores, un in parámetro.
Motivación
El compilador debe asumir que cualquier llamada al método en una instancia de struct puede modificar la
instancia. En realidad, se pasa una referencia grabable al método como this parámetro y habilita
completamente este comportamiento. Para permitir tales invocaciones en readonly variables, las invocaciones
se aplican a las copias temporales. Esto podría ser poco intuitivo y a veces obliga a los usuarios a abandonar
readonly por motivos de rendimiento.
Ejemplo: https://codeblog.jonskeet.uk/2014/07/16/micro-optimization-the-surprising-inefficiency-of-readonly-
fields/
Después de agregar compatibilidad para in los parámetros y ref readonly devolver el problema de la copia
defensiva se verá peor, ya que las variables de solo lectura volverán a ser más comunes.
Solución
Permita readonly el modificador en las declaraciones de struct, lo que provocaría que this se tratase como
in parámetro en todos los métodos de instancia de estructura, salvo para los constructores.
// OK
return $"X: {X}, Y: {Y}, Z: {Z}";
}
}
En concreto:
La identidad del IsReadOnlyAttribute tipo no es importante. De hecho, el compilador puede incrustarlo en el
ensamblado contenedor si es necesario.
De hecho, actualmente no se puede establecer una variable que no sea null null a menos que sea
explícitamente asignada o pasada por ref o out . Esto ayuda en gran medida a la legibilidad u otras
formas de análisis "puede ser un valor nulo aquí". 3. Sería difícil conciliar con la semántica "evaluar una vez"
de los accesos condicionales null. Ejemplo: obj.stringField?.RefExtension(...) -es necesario capturar una
copia de stringField para que la comprobación de valores NULL sea significativa, pero las asignaciones a
this dentro de RefExtension no se reflejarán de nuevo en el campo.
Una capacidad para declarar métodos de extensión en Structs que toman el primer argumento por referencia
era una solicitud de larga duración. Una de las consideraciones de bloqueo fue "¿Qué ocurre si el receptor no es
un valor l?".
Existe un precedente que también se podría llamar a cualquier método de extensión como método estático (a
veces es la única manera de resolver la ambigüedad). Esto indicaría que no se permiten los receptores de
valor r.
Por otro lado, existe la posibilidad de efectuar la invocación en una copia en situaciones similares cuando
intervienen los métodos de instancia de struct.
La razón por la que existe la "copia implícita" es que la mayoría de los métodos struct no modifican realmente el
struct mientras no es capaz de indicarlo. Por lo tanto, la solución más práctica era simplemente hacer la
invocación en una copia, pero esta práctica se conoce para perjudicar el rendimiento y provocar errores.
Ahora, con la disponibilidad de in los parámetros, es posible que una extensión señale el intento. Por lo tanto,
se puede resolver el dilema exigiendo que ref las extensiones se llamen con receptores que se pueden escribir,
mientras que in las extensiones permiten la copia implícita si es necesario.
in extensiones y genéricos.
El propósito de ref los métodos de extensión es mutar el receptor directamente o invocar a los miembros
mutantes. Por lo tanto, ref this T se permiten las extensiones mientras T esté restringida a una estructura.
Por otra parte, in existen métodos de extensión específicamente para reducir la copia implícita. Sin embargo,
cualquier uso de un in T parámetro tendrá que realizarse a través de un miembro de interfaz. Dado que todos
los miembros de interfaz se consideran mutables, cualquier uso de este tipo requeriría una copia. -En lugar de
reducir la copia, el efecto sería el contrario. Por lo tanto, in this T no se permite cuando T es un parámetro
de tipo genérico, independientemente de las restricciones.
Tipos válidos de métodos de extensión (Resumen):
Ahora se permiten las siguientes formas de this declaración en un método de extensión:
1. this T arg -extensión ByVal normal. (caso existente )
T puede ser cualquier tipo, incluidos los tipos de referencia o los parámetros de tipo. La instancia será la
misma variable después de la llamada. Permite conversiones implícitas de este tipo de conversión de
argumentos . Se puede llamar a en RValues.
in this T self - in Extension. T debe ser un tipo de struct real. La instancia será la misma variable
después de la llamada. Permite conversiones implícitas de este tipo de conversión de argumentos . Se
puede llamar a en RValues (se puede invocar en un Temp si es necesario).
ref this T self - ref Extension. T debe ser un tipo de estructura o un parámetro de tipo genérico
restringido para ser un struct. La invocación puede escribir en la instancia. Solo permite conversiones de
identidad. Se debe llamar al método LValue grabable. (nunca se invoca a través de un Temp).
Para todos los propósitos ref readonly , un local se trata como una readonly variable. La mayoría de las
restricciones en el uso son las mismas que con readonly los campos o in parámetros.
Por ejemplo, los campos de un in parámetro que tiene un tipo de estructura se clasifican de forma recursiva
como readonly variables.
static readonly ref Vector3 M1() => . . .
// OK.
Print(in r1);
// OK.
return ref r1;
}
Tenga en cuenta que Choice no es un reemplazo exacto de un ternario, ya que todos los argumentos deben
evaluarse en el sitio de llamada, lo que ha provocado errores y comportamientos inintuitivos.
Lo siguiente no funcionará según lo esperado:
Solución
Permite un tipo especial de expresión condicional que se evalúa como una referencia a uno de los argumentos
LValue basados en una condición.
Usar una ref expresión ternaria.
La sintaxis para el ref tipo de una expresión condicional es
<condition> ? ref <consequence> : ref <alternative>;
Del mismo modo que con la expresión condicional ordinaria solo <consequence> o <alternative> se evalúa
según el resultado de la expresión de condición booleana.
A diferencia de la expresión condicional ordinaria, ref expresión condicional:
requiere que <consequence> y <alternative> sean LValues.
ref la propia expresión condicional es un valor l y
ref la expresión condicional es grabable si tanto <consequence> como <alternative> son grabables
LValues
Ejemplos:
ref ternario es un valor l y, como tal, se puede pasar, asignar o devolver por referencia;
// pass by reference
foo(ref (arr != null ? ref arr[0]: ref otherArr[0]));
// return by reference
return ref (arr != null ? ref arr[0]: ref otherArr[0]);
// assign to
(arr != null ? ref arr[0]: ref otherArr[0]) = 1;
Se puede usar como receptor de una llamada al método y omitir la copia si es necesario.
// no copies
(arr != null ? ref arr[0]: ref otherArr[0]).StructMethod();
// invoked on a copy.
// The receiver is `readonly` because readOnlyField is readonly.
(arr != null ? ref arr[0]: ref obj.readOnlyField).StructMethod();
// only an example
// a regular ternary could work here just the same
int x = (arr != null ? ref arr[0]: ref otherArr[0]);
Inconvenientes
Puedo ver dos argumentos principales contra la compatibilidad mejorada para referencias y referencias de solo
lectura:
1. Los problemas que se solucionan aquí son muy antiguos. ¿Por qué solucionarlos repentinamente ahora,
especialmente porque no ayudaría el código existente?
Cuando encontramos C# y .net que se usan en nuevos dominios, algunos problemas se hacen más prominentes.
Como ejemplos de entornos que son más críticos que el promedio de las sobrecargas de cálculo, puedo Mostrar
escenarios de Cloud/Datacenter en los que se factura el cálculo y la capacidad de respuesta es una ventaja
competitiva.
Juegos/VR/AR con requisitos de tiempo real en latencia
Esta característica no sacrifica ninguno de los puntos fuertes existentes, como la seguridad de tipos, a la vez que
permite reducir las sobrecargas en algunos escenarios comunes.
2. ¿Podemos garantizar razonablemente que el destinatario de la llamada reproducirá las reglas cuando opte
por los readonly contratos?
Tenemos confianza similar cuando se usa out . Una implementación incorrecta de out puede provocar un
comportamiento no especificado, pero en realidad suele ocurrir.
La realización de las reglas de comprobación formal que le resultan familiares es ref readonly mitigar aún más
el problema de confianza.
Alternativas
En realidad, el diseño de la competencia principal es "no hacer nada".
Preguntas no resueltas
Design Meetings
https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-02-22.md
https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-03-01.md
https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-08-28.md
https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-09-25.md
https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-09-27.md
Aplicación del tiempo de compilación de seguridad
para los tipos de referencia
18/09/2021 • 32 minutes to read
Introducción
La razón principal de las reglas de seguridad adicionales cuando se trabaja con tipos como Span<T> y
ReadOnlySpan<T> es que dichos tipos se deben limitar a la pila de ejecución.
Hay dos razones por las Span<T> que y los tipos similares deben ser tipos de solo pila.
1. Span<T>es semánticamente un struct que contiene una referencia y un intervalo (ref T data, int length) .
Independientemente de la implementación real, las escrituras en dicho struct no serían atómicas. El
"desgarro" simultáneo de este tipo de estructura provocaría la posibilidad de length no coincidir con data ,
lo que provocaría accesos fuera del intervalo e infracciones de seguridad de tipos, lo que en última instancia
podría provocar daños en el montón GC en código aparentemente "seguro".
2. Algunas implementaciones de Span<T> contienen literalmente un puntero administrado en uno de sus
campos. Los punteros administrados no se admiten como campos de objetos de montón y el código que
administra para colocar un puntero administrado en el montón GC normalmente se bloquea en tiempo JIT.
Todos los problemas anteriores se solucionarían si las instancias de Span<T> están restringidas a existir solo en
la pila de ejecución.
Se produce un problema adicional debido a la composición. Por lo general, sería conveniente crear tipos de
datos más complejos que insertaría Span<T> ReadOnlySpan<T> las instancias de y. Estos tipos compuestos deben
ser Structs y compartir todos los peligros y requisitos de Span<T> . Como resultado, las reglas de seguridad que
se describen aquí deben verse como corresponda a toda la gama de tipos de referencia .
La especificación del lenguaje de borrador está pensada para garantizar que los valores de un tipo de referencia
se produzcan solo en la pila.
Designar un struct como referencia permite que el struct tenga campos de instancia de tipo REF y también hará
que todos los requisitos de los tipos de referencia se apliquen a la estructura.
Se realizará una medida adicional para evitar el uso de estructuras de tipo REF en compiladores que no están
familiarizados con las reglas de seguridad (esto incluye los compiladores de C# antes de la implementación de
esta característica).
Si no hay ninguna otra buena alternativa que funcione en los compiladores antiguos sin servicio, se Obsolete
agregará un atributo con una cadena conocida a todos los Structs de tipo Ref. Los compiladores que sepan usar
tipos de tipo REF no tendrán en cuenta esta forma determinada de Obsolete .
Una representación de metadatos típica:
[IsRefLike]
[Obsolete("Types with embedded references are not supported in this version of your compiler.")]
public struct TwoSpans<T>
{
// . . . .
}
Nota: no es el objetivo de hacerlo para que cualquier uso de tipos de tipo REF en los compiladores anteriores
produzca un error 100%. Esto es difícil de lograr y no es estrictamente necesario. Por ejemplo, siempre debería
haber una manera de evitar el Obsolete uso de código dinámico o, por ejemplo, crear una matriz de tipos de
tipo REF a través de la reflexión.
En concreto, si el usuario desea colocar realmente un Obsolete Deprecated atributo o en un tipo de referencia,
no tendrá ninguna opción que no emita el parámetro predefinido, ya que el Obsolete atributo no se puede
aplicar más de una vez.
Ejemplos:
SpanLikeType M1(ref SpanLikeType x, Span<byte> y)
{
// this is all valid, unconcerned with stack-referring stuff
var local = new SpanLikeType(y);
x = local;
return x;
}
// this is allowed
stackReferring2 = M1(ref stackReferring2, stackReferring1);
// this is allowed
param1 = new SpanLikeType(param2);
// this is allowed
stackReferring2 = param1;
}
// this is allowed
stackReferring3 = M1(ref stackReferring2, stackReferring1);
// this is allowed
M2(ref stackReferring3) = stackReferring2;
// this is allowed
return ref param1;
}
Especificación del lenguaje borrador
A continuación se describe un conjunto de reglas de seguridad para los tipos de referencia ( ref struct s) para
asegurarse de que los valores de estos tipos solo se producen en la pila. Sería posible un conjunto de reglas de
seguridad diferente y más sencillo si las variables locales no se pueden pasar por referencia. Esta especificación
también permitiría la reasignación segura de variables locales de referencia.
Información general
Asociamos con cada expresión en tiempo de compilación el concepto del ámbito al que se permite el escape de
la expresión, "Safe-to-escape". Del mismo modo, para cada valor l, se mantiene un concepto del ámbito al que se
permite que se escape una referencia, "ref-Safe-to-escape". Para una expresión lvalue determinada, pueden ser
diferentes.
Son análogos a "Safe to Return" de la característica Ref locals, pero es más específica. Cuando el valor de "Safe-
to-Return" de una expresión solo registra si (o no) puede escapar el método envolvente en su conjunto, los
registros de seguridad a escape a los que puede escapar (el ámbito en el que podría no escapar). El mecanismo
de seguridad básica se aplica como se indica a continuación. Dada una asignación desde una expresión E1 con
un ámbito de salida a escape S1, a una expresión (lvalue) E2 con un ámbito de seguridad a escape S2, es un
error si S2 es un ámbito más amplio que S1. Por construcción, los dos ámbitos S1 y S2 están en una relación de
anidamiento, ya que una expresión válida siempre es segura para devolver desde algún ámbito que incluya la
expresión.
En el momento en que es suficiente, para el análisis, admitir solo dos ámbitos: externo al método y el ámbito de
nivel superior del método. Esto se debe a que no se pueden crear valores de tipo REF con ámbitos internos y las
variables locales de referencia no admiten la reasignación. Sin embargo, las reglas pueden admitir más de dos
niveles de ámbito.
A continuación se indican las reglas precisas para calcular el estado de seguridad de la devolución de una
expresión y las reglas que rigen la legalidad de las expresiones.
Ref-Safe -to -escape
Ref-Safe-to-escape es un ámbito, que incluye una expresión lvalue, a la que es seguro para una referencia al
valor l a la que se debe escapar. Si ese ámbito es el método completo, se indica que una referencia a lvalue es
segura para devolver desde el método.
Safe -to -escape
Safe-to-escape es un ámbito, que incluye una expresión, a la que es seguro que el valor se escape. Si ese ámbito
es el método completo, se indica que el valor es seguro para volver del método.
Una expresión cuyo tipo no es un ref struct tipo es seguro para devolver desde todo el método envolvente.
En caso contrario, hacemos referencia a las reglas siguientes.
Parámetros
Un valor l que designa un parámetro formal es ref-Safe-to-escape (por referencia) de la manera siguiente:
Si el parámetro es un ref out parámetro, o in , ref-Safe-to-escape del método completo (por ejemplo,
mediante una return ref instrucción); en caso contrario,
Si el parámetro es el this parámetro de un tipo de estructura, es ref-Safe-to-escape en el ámbito de nivel
superior del método (pero no en todo el método); Ejemplo de
De lo contrario, el parámetro es un parámetro de valor y es ref-Safe-to-escape al ámbito de nivel superior del
método (pero no al propio método).
Una expresión que es un valor r que designa el uso de un parámetro formal es Safe-to-escape (por valor) desde
el método completo (por ejemplo, mediante una return instrucción). Esto también se aplica al this
parámetro.
Locals
Un valor l que designa una variable local es ref-Safe-to-escape (por referencia) de la manera siguiente:
Si la variable es una ref variable, su referencia de ref-Safe-to-escape se toma de la referencia de ref-Safe-
to-escape de la expresión de inicialización; de lo contrario,
La variable es ref-Safe-to-escape el ámbito en el que se declaró.
Una expresión que es un valor r que designa el uso de una variable local es segura a escape (por valor) de la
manera siguiente:
Sin embargo, la regla general anterior, una variable cuyo tipo no es un ref struct tipo es seguro para
devolver desde todo el método envolvente.
Si la variable es una variable de iteración de un foreach bucle, el ámbito de seguridad a escape de la
variable es el mismo que el de la foreach expresión del bucle de seguridad.
Una variable local de ref struct tipo y no inicializada en el punto de la declaración es segura para devolver
desde todo el método de inclusión.
De lo contrario, el tipo de la variable es un ref struct tipo, y la declaración de la variable requiere un
inicializador. El ámbito de seguridad a escape de la variable es el mismo que el del inicializador.
Referencia de campo
Un valor l que designa una referencia a un campo, e.F , es ref-Safe-to-escape (por referencia) de la manera
siguiente:
Si e es de un tipo de referencia, es ref-Safe-to-escape del método completo; de lo contrario, es.
Si e es de un tipo de valor, su ref-Safe-to-escape se toma de ref-Safe-to-escape de e .
Un valor r que designe una referencia a un campo, e.F , tiene un ámbito de seguridad a escape que es igual
que el de seguridad de salida de e .
Operadores incluidos ?:
La aplicación de un operador definido por el usuario se trata como una invocación de método.
Para un operador que produce un valor r, como e1 + e2 o c ? e1 : e2 , el valor de Safe-to-escape del
resultado es el ámbito más restringido entre el valor de Safe-to-escape de los operandos del operador. Como
consecuencia, para un operador unario que produce un valor r, como +e , el valor de Safe-to-escape del
resultado es el de seguridad del operando.
Para un operador que produce un valor l, como c ? ref e1 : ref e2
ref-Safe-to-escape del resultado es el ámbito más restringido entre ref-Safe-to-escape de los operandos del
operador.
el valor de Safe-to-escape de los operandos debe coincidir, y es el valor de Safe-to-escape del valor l
resultante.
Invocación de método
Un valor l resultante de una invocación de método de devolución de referencia e1.M(e2, ...) es ref-Safe-to-
escape el menor de los siguientes ámbitos:
Todo el método envolvente
ref-Safe-to-escape de todas las ref out expresiones de argumentos y (excluyendo el receptor)
Para cada in parámetro del método, si hay una expresión correspondiente que sea un valor l, su referencia-
Safe-to-escape, de lo contrario, el ámbito de inclusión más próximo
el carácter de escape seguro de todas las expresiones de argumentos (incluido el receptor)
or
Un valor r que resulta de una invocación de método e1.M(e2, ...) es seguro para el escape de la menor de los
siguientes ámbitos:
Todo el método envolvente
el carácter de escape seguro de todas las expresiones de argumentos (incluido el receptor)
Un valor r
Un valor r es ref-Safe-to-escape del ámbito de inclusión más cercano. Esto sucede, por ejemplo, en una
invocación como, M(ref d.Length) donde d es de tipo dynamic . También es coherente con (y quizás
subsumes) nuestro control de los argumentos correspondientes a in los parámetros.
Invocaciones de propiedad
Una invocación de propiedad ( get o set ) se trata como una invocación de método del método subyacente
por parte de las reglas anteriores.
stackalloc
Una expresión stackalloc es un valor r que es seguro para el ámbito de nivel superior del método, pero no del
propio método completo.
Invocaciones del constructor
Una new expresión que invoca un constructor obedece las mismas reglas que una invocación de método que se
considera que devuelve el tipo que se está construyendo.
Además, el valor de Safe-to-escape no es más ancho que el más pequeño de los argumentos y operandos de la
expresión de inicializador de objeto, de forma recursiva, si el inicializador está presente.
Span (constructor)
El lenguaje depende de Span<T> que no tenga un constructor con el formato siguiente:
Este tipo de constructor hace Span<T> que se usen como campos que no se pueden distinguir de un ref
campo. Las reglas de seguridad descritas en este documento dependen de ref los campos que no son una
construcción válida en C# o .net.
Expresiones default
Restricciones de lenguaje
Queremos asegurarnos de que ninguna ref variable local y ninguna variable de ref struct tipo hacen
referencia a la memoria de la pila o a las variables que ya no están activas. Por lo tanto, tenemos las siguientes
restricciones de lenguaje:
Ni un parámetro ref, ni una variable local de tipo REF, ni un parámetro o una variable local de un
ref struct tipo se pueden levantar en una función lambda o local.
Ni un parámetro ref ni un parámetro de un ref struct tipo pueden ser un argumento en un método
iterador o un async método.
Ni una variable local de tipo REF ni una variable local de un ref struct tipo pueden estar en el ámbito
de una yield return instrucción o una await expresión.
Un ref struct tipo no se puede usar como un argumento de tipo o como un tipo de elemento en un
tipo de tupla.
Un ref struct tipo no puede ser el tipo declarado de un campo, salvo que puede ser el tipo declarado
de un campo de instancia de otro ref struct .
Un ref struct tipo no puede ser el tipo de elemento de una matriz.
ref struct No se puede aplicar la conversión boxing a un valor de un tipo:
No hay ninguna conversión de un ref struct tipo al tipo object o al tipo System.ValueType .
Un ref struct tipo no se puede declarar para implementar ninguna interfaz
No se puede llamar a ningún método de instancia declarado en object o en, System.ValueType pero
que no se invalide en un ref struct tipo, con un receptor de ese ref struct tipo.
No se puede capturar ningún método de instancia de un ref struct tipo mediante la conversión de
método a un tipo de delegado.
En el caso de una reasignación de referencia ref e1 = ref e2 , el parámetro ref-Safe-to-escape de e2
debe ser al menos tan ancho como el de la referencia de ref-Safe-to-escape de e1 .
En el caso de una instrucción Ref Return return ref e1 , el valor de ref-Safe-to-escape de e1 debe ser
ref-Safe-to-escape desde el método completo. (TODO: ¿también necesitamos una regla que e1 deba ser
segura para el escape desde el método completo, o que sea redundante?)
En el caso de una instrucción return return e1 , el valor Safe-to-escape de e1 debe ser seguro para el
método completo.
En el caso de una asignación e1 = e2 , si el tipo de e1 es un ref struct tipo, el carácter de escape
seguro de e2 debe ser al menos tan ancho como un ámbito de seguridad de salida de e1 .
En el caso de una invocación de método si hay un ref out argumento o de un ref struct tipo
(incluido el receptor), con Safe-to-escape E1, ningún argumento (incluido el receptor) puede tener un
método Safe-to-escape más estrecho que E1. Ejemplo
Una función local o una función anónima no puede hacer referencia a un parámetro local o de
ref struct tipo declarado en un ámbito de inclusión.
Problema abier to: Se necesita una regla que nos permita generar un error cuando se necesita desbordar
un valor de pila de un ref struct tipo en una expresión Await, por ejemplo en el código.
Explicaciones
Estas explicaciones y ejemplos ayudan a explicar por qué muchas de las reglas de seguridad anteriores existen.
Los argumentos de método deben coincidir
Al invocar un método en el que hay out un ref parámetro, que es un que ref struct incluye el receptor,
todos los ref struct deben tener la misma duración. Esto es necesario porque C# debe tomar todas sus
decisiones sobre la seguridad de la duración en función de la información disponible en la firma del método y la
duración de los valores en el sitio de la llamada.
Cuando hay ref parámetros que son ref struct , entonces hay el posibilidad que podrían cambiar en torno a
su contenido. Por lo tanto, en el sitio de llamada, debemos asegurarnos de que todos estos swaps potenciales
sean compatibles. Si el lenguaje no aplicó, se permitirá un código incorrecto como el siguiente.
ref struct S
{
public Span<int> Span;
void Broken(ref S s)
{
Span<int> span = stackalloc int[1];
// The result of a stackalloc is now stored in s.Span and escaped to the caller
// of Broken
s.Set(span);
}
// Legal
ref int GetParam(ref int p) => ref p;
}
La razón de esta restricción realmente tiene poco que hacer con la struct invocación de miembros. Hay
algunas reglas que deben realizarse con respecto a la invocación de miembros en struct los miembros en los
que el receptor es un valor r. Pero es muy accesible.
La razón de esta restricción es realmente la invocación de la interfaz. En concreto, se refiere a si el siguiente
ejemplo debe o no debe compilarse;
interface I1
{
ref int Get();
}
Considere el caso en el que T se crea una instancia como struct . Si el this parámetro es Ref-Safe-to-
escape, el valor devuelto de p.Get podría apuntar a la pila (concretamente podría ser un campo dentro del tipo
del que se ha creado una instancia T ). Esto significa que el lenguaje no puede permitir que se compile este
ejemplo, ya que podría estar devolviendo un ref a una ubicación de la pila. Por otro lado, si this no es Ref-
Safe-to-escape, p.Get no puede hacer referencia a la pila y, por lo tanto, es seguro devolver.
Este es el motivo por el que la elusión de this en un struct es realmente una de las interfaces. Se puede
realizar de forma absoluta en el trabajo, pero tiene una desventaja. Finalmente, el diseño apareció en favor de
que las interfaces sean más flexibles.
Sin embargo, es posible que esto se Relájese en el futuro.
Consideraciones futuras
Longitud de un intervalo de <T> valores de referencia
Aunque no es legal hoy en día, hay casos en los que la creación de una longitud de una Span<T> instancia sobre
un valor sería beneficiosa:
void RefExample()
{
int x = ...;
Esta característica resulta más atractiva si se elevan las restricciones en los búferes de tamaño fijo , ya que esto
permitiría Span<T> instancias de una longitud incluso mayor.
Si alguna vez es necesario bajar esta ruta de acceso, el lenguaje podría dar cabida al garantizar que dichas
Span<T> instancias solo estaban orientadas hacia abajo. Es decir, solo estaban seguros para el ámbito en el que
se crearon. Esto garantiza que el lenguaje nunca tenía que considerar un ref valor que escape un método a
través ref struct de una devolución o un campo de ref struct . Esto podría requerir también cambios
adicionales para reconocer tales constructores como ref la captura de un parámetro de esta manera.
Argumentos con nombre no finales
18/09/2021 • 5 minutes to read
Resumen
Permita que los argumentos con nombre se utilicen en la posición no final, siempre que se usen en su posición
correcta. Por ejemplo: DoSomething(isEmployed:true, name, age); .
Motivación
La principal motivación es evitar escribir información redundante. Es habitual asignar un nombre a un
argumento que sea un literal (por ejemplo null , true ) con el fin de aclarar el código, en lugar de pasar
argumentos desordenados. No se permite actualmente ( CS1738 ) a menos que todos los argumentos
siguientes también se denominen.
DoSomething(isEmployed:true, name, age); // currently disallowed, even though all arguments are in position
// CS1738 "Named argument specifications must appear after all fixed arguments have been specified"
Diseño detallado
En la sección § 7.5.1 (listas de argumentos), la especificación indica actualmente:
Un argumento con un argumento-Name se conoce como argumento con nombre , mientras que un
argumento sin el argumento-Name es un argumento posicional . Es un error que un argumento
posicional aparezca después de un argumento con nombre en una lista de argumentos.
La propuesta es quitar este error y actualizar las reglas para buscar el parámetro correspondiente para un
argumento (§ 7.5.1.1):
Argumentos en la lista de argumentos de constructores de instancia, métodos, indizadores y delegados:
[reglas existentes]
Un argumento sin nombre corresponde a ningún parámetro cuando está después de un argumento con
nombre fuera de posición o un argumento params con nombre.
En concreto, esto impide que se invoque void M(bool a = true, bool b = true, bool c = true, ); con
M(c: false, valueB); . El primer argumento se usa fuera de la posición (el argumento se usa en la primera
posición, pero el parámetro denominado "c" está en la tercera posición), por lo que los siguientes argumentos
deben tener nombre.
En otras palabras, los argumentos con nombre no finales solo se permiten cuando el nombre y la posición dan
como resultado el mismo parámetro correspondiente.
Inconvenientes
Esta propuesta agrava los matices existentes con argumentos con nombre en la resolución de sobrecarga. Por
ejemplo:
void M2()
{
M(3, 4);
M(y: 3, x: 4); // Invokes M(int, int)
M(y: 3, 4); // Invokes M<T>(T, int)
}
void M2()
{
M(3, 4);
M(x: 3, y: 4); // Invokes M(int, int)
M(3, y: 4); // Invokes M<T>(int, T)
}
Del mismo modo, si tiene dos métodos void M(int a, int b) y void M(int x, string y) , la invocación errónea
producirá M(x: 1, 2) un diagnóstico basado en la segunda sobrecarga ("no se puede convertir de ' int ' a '
String '"). Este problema ya existe cuando el argumento con nombre se usa en una posición final.
Alternativas
Hay un par de alternativas a tener en cuenta:
El estado quo
Proporcionar asistencia IDE para rellenar todos los nombres de los argumentos finales cuando se escribe un
nombre específico en el centro.
Ambos sufren un mayor nivel de detalle, ya que introducen varios argumentos con nombre, incluso si solo se
necesita un nombre de literal al principio de la lista de argumentos.
Preguntas no resueltas
Design Meetings
La característica se analizó brevemente en LDM el 16 de mayo de 2017, con aprobación en principio (Aceptar
para pasar a propuesta/prototipo). También se analizó brevemente el 28 de junio de 2017.
Está relacionado con la discusión inicial https://github.com/dotnet/csharplang/issues/518 relacionada con el
problema experto https://github.com/dotnet/csharplang/issues/570
privado protegido
18/09/2021 • 19 minutes to read
[x] propuesto
[x] prototipo: completo
[x] implementación: completa
[x] Especificación: completa
Resumen
Exponga el protectedAndInternal nivel de accesibilidad de CLR en C# como private protected .
Motivación
Hay muchas circunstancias en las que una API contiene miembros que solo están diseñados para ser
implementados y utilizados por las subclases contenidas en el ensamblado que proporciona el tipo. Aunque CLR
proporciona un nivel de accesibilidad para ese propósito, no está disponible en C#. Por lo tanto, los propietarios
de la API deben usar internal la protección y autodisciplina o un analizador personalizado, o bien usar
protected con documentación adicional que explique que, mientras que el miembro aparece en la
documentación pública para el tipo, no está diseñado para formar parte de la API pública. Para ver ejemplos de
este último, consulte miembros de Roslyn CSharpCompilationOptions cuyos nombres empiezan por Common .
Proporcionar compatibilidad directamente con este nivel de acceso en C# permite expresar estas circunstancias
de forma natural en el lenguaje.
Diseño detallado
Modificador de acceso private protected
Se propone agregar una nueva combinación de modificador private protected de acceso (que puede aparecer
en cualquier orden entre los modificadores). Esto se asigna a la noción de CLR de protectedAndInternal y presta
la misma sintaxis que se usa actualmente en C++/CLI.
Se puede tener acceso a un miembro declarado private protected dentro de una subclase de su contenedor si
esa subclase está en el mismo ensamblado que el miembro.
Modificamos la especificación del lenguaje como se indica a continuación (adiciones en negrita). Los números
de sección no se muestran a continuación, ya que pueden variar en función de la versión de la especificación en
la que está integrado.
Dependiendo del contexto en el que tenga lugar una declaración de miembro, solo se permiten
determinados tipos de accesibilidad declarada. Además, cuando una declaración de miembro no incluye
modificadores de acceso, el contexto en el que se produce la declaración determina la accesibilidad
declarada predeterminada.
Los espacios de nombres tienen implícitamente una accesibilidad declarada pública. No se permiten
modificadores de acceso en las declaraciones de espacio de nombres.
Los tipos declarados directamente en unidades de compilación o espacios de nombres (en lugar de dentro
de otros tipos) pueden tener accesibilidad declarada pública o interna y tener como valor predeterminado la
accesibilidad declarada interna.
Los miembros de clase pueden tener cualquiera de los cinco tipos de accesibilidad declarada y tienen como
valor predeterminado la accesibilidad declarada privada. [Nota: un tipo declarado como miembro de una
clase puede tener cualquiera de los cinco tipos de accesibilidad declarada, mientras que un tipo declarado
como miembro de un espacio de nombres solo puede tener acceso declarado público o interno. finalizar
Nota]
Los miembros de struct pueden tener accesibilidad declarada pública, interna o privada y de forma
predeterminada en Private, ya que los Structs están sellados implícitamente. Los miembros de estructura
introducidos en un struct (es decir, no heredados por ese struct) no pueden tener accesibilidad declarada
protegida , interna protegida o privada . [Nota: un tipo declarado como miembro de un struct puede tener
una accesibilidad declarada pública, interna o privada, mientras que un tipo declarado como miembro de un
espacio de nombres solo puede tener una accesibilidad declarada pública o interna. finalizar Nota]
Los miembros de interfaz tienen implícitamente una accesibilidad declarada pública. No se permiten
modificadores de acceso en las declaraciones de miembros de interfaz.
Los miembros de enumeración tienen implícitamente una accesibilidad declarada pública. No se permiten
modificadores de acceso en las declaraciones de miembros de enumeración.
Cuando se tiene acceso a un miembro de instancia protegido o privado protegido fuera del texto del
programa de la clase en la que se declara, y cuando se tiene acceso a un miembro de instancia interno
protegido fuera del texto del programa en el que se declara, el acceso se realiza dentro de una declaración
de clase que se deriva de la clase en la que se declara. Además, el acceso debe realizarse a través de una
instancia de ese tipo de clase derivada o de un tipo de clase construido a partir de él. Esta restricción evita
que una clase derivada tenga acceso a miembros protegidos de otras clases derivadas, incluso cuando los
miembros se heredan de la misma clase base.
Los modificadores de acceso permitidos y el acceso predeterminado de una declaración de tipo dependen
del contexto en el que se produce la declaración (§ 9.5.2):
Los tipos declarados en unidades de compilación o espacios de nombres pueden tener acceso público o
interno. El valor predeterminado es acceso interno.
Los tipos declarados en las clases pueden tener acceso public, protected internal, Private Protected,
Protected, Internal o Private. El valor predeterminado es Private Access.
Los tipos declarados en Structs pueden tener acceso público, interno o privado. El valor predeterminado es
Private Access.
Una clase estática no debe incluir un modificador Sealed o Abstract. (Sin embargo, puesto que no se puede
crear una instancia de una clase estática o derivarse de ella, se comporta como si fuera Sealed y Abstract).
Una clase estática no debe incluir una especificación de clase base (§ 16.2.5) y no puede especificar
explícitamente una clase base o una lista de interfaces implementadas. Una clase estática hereda
implícitamente del tipo Object.
Una clase estática solo debe contener miembros estáticos (§ 16.4.8). [Nota: todas las constantes y los tipos
anidados se clasifican como miembros estáticos. finalizar Nota]
Una clase estática no debe tener miembros con accesibilidad declarada interna protegida , privada o
protegida.
Una declaración de miembro de clase puede tener cualquiera de los cincotipos posibles de accesibilidad
declarada (§ 9.5.2): Public, Private Protected , protected internal, Protected, Internal o Private. A excepción
de las combinaciones protegidas internas y privadas protegidas, se trata de un error en tiempo de
compilación para especificar más de un modificador de acceso. Cuando una declaración de miembro de
clase no incluye modificadores de acceso, se supone Private.
Los tipos no anidados pueden tener una accesibilidad declarada pública o interna y tener la accesibilidad
declarada interna de forma predeterminada. Los tipos anidados también pueden tener estas formas de
accesibilidad declarada, además de una o varias formas adicionales de accesibilidad declarada, dependiendo
de si el tipo contenedor es una clase o un struct:
Un tipo anidado que se declara en una clase puede tener cincoformas de accesibilidad declarada (Public,
Private Protected , protected internal, Protected, Internal o Private) y, al igual que otros miembros de clase,
tiene como valor predeterminado la accesibilidad declarada privada.
Un tipo anidado que se declara en un struct puede tener cualquiera de las tres formas de accesibilidad
declarada (Public, Internal o Private) y, al igual que otros miembros de struct, tiene como valor
predeterminado la accesibilidad declarada privada.
El método invalidado por una declaración de invalidación se conoce como el método base invalidado para
un método de invalidación M declarado en una clase C, el método base invalidado se determina
examinando cada tipo de clase base de C, empezando por el tipo de clase base directa de C y continuando
con cada tipo de clase base directo sucesivo, hasta que en un tipo de clase base determinado se encuentre al
menos un método accesible que tenga la misma firma que M después de la sustitución de argumentos de
tipo. Con el fin de localizar el método base invalidado, se considera que un método es accesible si es público,
si está protegido, si está protegido internamente o si es interno o privado protegido y declarado en el
mismo programa que C.
El uso de los modificadores de descriptor de acceso se rige por las siguientes restricciones:
No se puede usar un modificador de descriptor de acceso en una interfaz o en una implementación explícita
de un miembro de interfaz.
En el caso de una propiedad o un indizador que no tenga modificador override, solo se permite un
modificador de descriptor de acceso si la propiedad o indizador tiene un descriptor de acceso get y set y, a
continuación, solo se permite en uno de estos descriptores de acceso.
En el caso de una propiedad o indizador que incluya un modificador override, un descriptor de acceso debe
coincidir con el modificador de descriptor de acceso, si existe, del descriptor de acceso que se va a
reemplazar.
El modificador de descriptor de acceso debe declarar una accesibilidad que sea estrictamente más restrictiva
que la accesibilidad declarada de la propiedad o el propio indexador. Para ser precisos:
Si la propiedad o el indexador tienen una accesibilidad declarada Public, el modificador de descriptor
de acceso puede ser Private Protected , Internal Protected, Internal, Protected o Private.
Si la propiedad o indizador tiene una accesibilidad declarada de Internal Protected, el modificador de
descriptor de acceso puede ser Private Protected , Internal, Protected o Private.
Si la propiedad o el indexador tienen una accesibilidad declarada de Internal o Protected, el
modificador de descriptor de acceso debe ser Private Protected o Private.
Si la propiedad o indizador tiene una accesibilidad declarada de Private Protected, el
modificador de descriptor de acceso debe ser privado.
Si la propiedad o indizador tiene una accesibilidad declarada de Private, no se puede usar ningún
modificador de descriptor de acceso.
Puesto que no se admite la herencia para Structs, la accesibilidad declarada de un miembro de struct no
puede ser Protected, Private Protected o protected internal.
Inconvenientes
Al igual que con cualquier característica de lenguaje, debemos cuestionar si se reembolsa la complejidad
adicional del lenguaje en la claridad adicional que se ofrece al cuerpo de los programas de C# que se
beneficiarían de la característica.
Alternativas
Una alternativa sería el aprovisionamiento de una API que combina un atributo y un analizador. El programador
coloca el atributo en un internal miembro para indicar que el miembro está diseñado para usarse solo en las
subclases y el analizador comprueba que se obedecen esas restricciones.
Preguntas no resueltas
La implementación se completa en gran medida. El único elemento de trabajo abierto está borrando una
especificación correspondiente para VB.
Design Meetings
TBD
Expresiones de referencia condicional
18/09/2021 • 3 minutes to read
El patrón de enlazar una variable de referencia a una u otra expresión condicional no está actualmente en C#.
La solución habitual es introducir un método como:
Tenga en cuenta que esto no es un reemplazo exacto de ternario, ya que todos los argumentos deben evaluarse
en el sitio de llamada.
Lo siguiente no funcionará según lo esperado:
El intento anterior con "Choice" se puede escribir correctamente con Ref ternario como:
La diferencia con respecto a la elección es que se tiene acceso a las expresiones de consecuencia y alternativas
de una manera realmente condicional, por lo que no vemos un bloqueo si arr == null
La referencia ternaria es simplemente un ternario, donde tanto la alternativa como la consecuencia son refs. De
forma natural, será necesario que los operandos de consecuencia/alternativos sean LValues. También se
requerirá que la consecuencia y la alternativa tengan tipos que sean convertibles entre sí.
El tipo de la expresión se calculará de forma similar a la del ternario normal. (Por ejemplo: en caso de que la
consecuencia y la alternativa tengan identidad convertible, pero tipos diferentes, se aplicarán las reglas de
combinación de tipos existentes.
De forma segura a la devolución se asumirán con cautela los operandos condicionales. Si no es seguro devolver
todo lo que no es seguro para devolver.
Ref ternario es un valor l y, como tal, se puede pasar, asignar o devolver por referencia;
// pass by reference
foo(ref (arr != null ? ref arr[0]: ref otherArr[0]));
// return by reference
return ref (arr != null ? ref arr[0]: ref otherArr[0]);
// assign to
(arr != null ? ref arr[0]: ref otherArr[0]) = 1;
La referencia ternaria se puede usar también en un contexto normal (no Ref). Aunque no sería común, ya que
también podía usar un ternario normal.
Notas de implementación:
La complejidad de la implementación parece ser el tamaño de una corrección de errores de moderado a grande.
-I. E no es muy caro. No creo que sea necesario realizar cambios en la sintaxis o el análisis. No hay ningún efecto
en los metadatos o la interoperabilidad. La característica se basa completamente en expresiones. No hay ningún
efecto en la depuración/PDB
Permitir separador de dígitos después del 0B o 0x
18/09/2021 • 2 minutes to read
En C# 7,2, ampliamos el conjunto de lugares en los que los separadores de dígitos (el carácter de subrayado)
pueden aparecer en literales enteros. A partir de C# 7,0, se permiten separadores entre los dígitos de un literal.
Ahora, en C# 7,2, también se permiten separadores de dígitos antes del primer dígito significativo de un literal
binario o hexadecimal, después del prefijo.
No se permite que un literal entero decimal tenga un carácter de subrayado inicial. Un token como _123 es un
identificador.
Restricción de tipo no administrado
18/09/2021 • 8 minutes to read
Resumen
La característica de restricción no administrada proporcionará el cumplimiento del lenguaje a la clase de tipos
conocidos como "tipos no administrados" en la especificación del lenguaje C#. Esto se define en la sección 18,2
como un tipo que no es un tipo de referencia y no contiene campos de tipo de referencia en ningún nivel de
anidamiento.
Motivación
La motivación principal es facilitar la creación de código de interoperabilidad de bajo nivel en C#. Los tipos no
administrados son uno de los bloques de creación principales del código de interoperabilidad, pero la falta de
compatibilidad en genéricos hace que sea imposible crear rutinas reutilizables en todos los tipos no
administrados. En su lugar, se obliga a los desarrolladores a crear el mismo código de placa de caldera para cada
tipo no administrado en su biblioteca:
Para habilitar este tipo de escenario, el lenguaje introducirá una nueva restricción:
Esta restricción solo se puede cumplir a través de tipos que se ajustan a la definición de tipo no administrado en
la especificación del lenguaje C#. Otra manera de examinarlo es que un tipo satisface la restricción no
administrada si también se puede utilizar como puntero.
Los parámetros de tipo con la restricción no administrada pueden usar todas las características disponibles para
los tipos no administrados: punteros, fijos, etc...
Esta restricción también hará posible tener conversiones eficaces entre los datos estructurados y los flujos de
bytes. Se trata de una operación que es habitual en las pilas de red y en las capas de serialización:
Span<byte> Convert<T>(ref T value) where T : unmanaged
{
...
}
Estas rutinas son ventajosas porque son provably seguras en tiempo de compilación y la asignación es gratuita.
Los autores de interoperabilidad hoy no pueden hacerlo (aunque se encuentra en una capa en la que el
rendimiento es crítico). En su lugar, deben confiar en la asignación de rutinas que tienen comprobaciones en
tiempo de ejecución costosas para comprobar que los valores se han desadministrado correctamente.
Diseño detallado
El lenguaje introducirá una nueva restricción denominada unmanaged . Para satisfacer esta restricción, un tipo
debe ser una estructura y todos los campos del tipo deben corresponder a una de las siguientes categorías:
Tienen el tipo sbyte ,,, byte short ushort , int , uint , long , ulong , char , float , double ,
decimal , bool IntPtr o UIntPtr .
Ser cualquier enum tipo.
Ser un tipo de puntero.
Ser una estructura definida por el usuario que cumpla la unmanaged restricción.
Los campos de instancia generados por el compilador, como los que respaldan las propiedades implementadas
automáticamente, también deben cumplir estas restricciones.
Por ejemplo:
// Unmanaged type
struct Point
{
int X;
int Y {get; set;}
}
La unmanaged restricción no se puede combinar con struct , class o new() . Esta restricción se deriva del
hecho de que implica que, por lo unmanaged struct tanto, las demás restricciones no tienen sentido.
unmanagedCLR no aplica la restricción, solo en el lenguaje. Para evitar que otros lenguajes lo utilicen, los
métodos que tienen esta restricción estarán protegidos por mod-req. Esto impedirá que otros lenguajes usen
argumentos de tipo que no son tipos no administrados.
El token unmanaged de la restricción no es una palabra clave ni una palabra clave contextual. En su lugar, es
como var en que se evalúa en esa ubicación y:
Enlazar a un tipo definido por el usuario o al que se hace referencia llamado unmanaged : se tratará como
cualquier otra restricción de tipo con nombre que se trate.
Enlazar a ningún tipo: se interpretará como la unmanaged restricción.
En el caso de que haya un tipo denominado unmanaged y que esté disponible sin calificación en el contexto
actual, no habrá forma de usar la unmanaged restricción. Esto se asemeja a las reglas que rodean la característica
var y los tipos definidos por el usuario del mismo nombre.
Inconvenientes
El principal inconveniente de esta característica es que sirve a un pequeño número de desarrolladores:
normalmente los autores o marcos de la biblioteca de bajo nivel. Por lo tanto, se pierde un tiempo de lenguaje
precioso para un pequeño número de desarrolladores.
Sin embargo, estos marcos de trabajo suelen ser la base de la mayoría de las aplicaciones .NET. Por lo tanto, el
rendimiento y la exactitud de WINS en este nivel pueden tener un efecto de rizo en el ecosistema de .NET. Esto
hace que la característica merezca la pena considerar incluso con la audiencia limitada.
Alternativas
Hay un par de alternativas a tener en cuenta:
El estado quo: la característica no está justificada por sus propios méritos y los desarrolladores siguen
usando el comportamiento de participación implícita.
Preguntas
Representación de metadatos
El lenguaje F # codifica la restricción en el archivo de signatura, lo que significa que C# no puede volver a usar
su representación. Se debe elegir un nuevo atributo para esta restricción. Además, un método que tiene esta
restricción debe estar protegido por mod-req.
Bytes entre bits y no administrados
El lenguaje F # tiene una característica muy similar que usa la palabra clave Unmanaged. El nombre que se va a
representar como bits/bytes proviene del uso en Midori. Puede que le interese buscar la precedencia aquí y usar
en su lugar no administrada.
Solución de El lenguaje que decide usar no administrado
Comprobador
¿Es necesario actualizar el comprobador/tiempo de ejecución para comprender el uso de punteros a parámetros
de tipo genérico? ¿O puede simplemente trabajar como está sin cambios?
Solución de No se necesitan cambios. Todos los tipos de puntero son simplemente no comprobables.
Design Meetings
N/D
fixed Los campos de indexación no deben requerir
el anclaje, independientemente del contexto
movible/unmovible.
18/09/2021 • 2 minutes to read
El cambio tiene el tamaño de una corrección de errores. Puede estar en 7,3 y no entra en conflicto con cualquier
dirección que se tome más. Este cambio es solo para permitir que el siguiente escenario funcione aunque s se
pueda mover. Ya es válido cuando s no se va a mover.
Nota: en cualquier caso, sigue siendo necesario el unsafe contexto. Es posible leer datos no inicializados o
incluso fuera del intervalo. Que no cambia.
unsafe struct S
{
public fixed int myFixedField[10];
}
class Program
{
static S s;
El "desafío" principal que veo aquí es cómo explicar la flexibilización en las especificaciones. En concreto, dado
que lo siguiente seguiría necesitando el anclaje. (dado s que es movible y usamos explícitamente el campo
como puntero)
unsafe struct S
{
public fixed int myFixedField[10];
}
class Program
{
static S s;
Una de las razones por las que se requiere el anclaje del destino cuando es movible es el artefacto de nuestra
estrategia de generación de código,-siempre se convierte en un puntero no administrado y, por tanto, se obliga
al usuario a anclar la fixed instrucción Via. Sin embargo, la conversión a no administrada no es necesaria
cuando se realiza la indexación. La misma expresión matemática de puntero no seguro es igualmente aplicable
cuando tenemos el receptor en forma de puntero administrado. Si lo hacemos, la referencia intermedia se
administra (seguimiento de GC) y el anclaje no es necesario.
El cambio https://github.com/dotnet/roslyn/pull/24966 es una PR de prototipo que relaja este requisito.
Instrucción fixed basada en patrones
18/09/2021 • 5 minutes to read
Resumen
Introduzca un patrón que permita a los tipos participar en las fixed instrucciones.
Motivación
El lenguaje proporciona un mecanismo para anclar datos administrados y obtener un puntero nativo al búfer
subyacente.
El conjunto de tipos que pueden participar en fixed está codificado y limitado a las matrices y System.String .
Los tipos "especiales" de codificar no se escalan cuando ImmutableArray<T> Span<T> se introducen nuevos
primitivos como, Utf8String .
Además, la solución actual para se System.String basa en una API bastante rígida. La forma de la API implica
que System.String es un objeto contiguo que incrusta los datos codificados en UTF16 en un desplazamiento
fijo del encabezado de objeto. Este enfoque se ha encontrado problemático en varias propuestas que podrían
requerir cambios en el diseño subyacente. Sería conveniente poder cambiar a algo más flexible que Desacople el
System.String objeto de su representación interna con el fin de interoperabilidad no administrada.
Diseño detallado
Patrón
Un "fijo" viable basado en patrones debe:
Proporcione las referencias administradas para anclar la instancia e inicializar el puntero (preferiblemente es
la misma referencia).
Transmite de forma inequívoca el tipo del elemento no administrado (es decir, "Char" para "String").
Prescribe el comportamiento en caso "vacío" cuando no hay nada al que hacer referencia.
No debe enviar los creadores de la API hacia las decisiones de diseño que perjudiquen el uso del tipo fuera
de fixed .
Creo que lo anterior se podría satisfacer reconociendo un miembro de devolución especial denominado:
ref [readonly] T GetPinnableReference() .
Para que la instrucción la use, fixed deben cumplirse las siguientes condiciones:
1. Solo se proporciona un miembro de este tipo para un tipo.
2. Devuelve por ref o ref readonly . ( readonly se permite para que los autores de tipos
inmutables/ReadOnly puedan implementar el patrón sin agregar la API grabable que se podría usar en
código seguro)
3. T es un tipo no administrado. (dado que T* se convierte en el tipo de puntero. La restricción se expande de
forma natural si se expande la noción de "no administrada")
4. Devuelve administrado nullptr cuando no hay datos para anclar, probablemente la manera más barata de
transmitir Emptiness. (tenga en cuenta que "" cadena devuelve una referencia a "\ 0", ya que las cadenas
terminan en null)
Como alternativa #3 , podemos permitir que el resultado en casos vacíos sea no definido o específico de la
implementación. Sin embargo, eso puede hacer que la API sea más peligrosa y propenso a abusos y cargas de
compatibilidad no intencionadas.
Traducción
fixed(byte* ptr = thing)
{
// <BODY>
}
se convierte en el siguiente pseudocódigo (no todos los elementos que se van a expresar en C#)
byte* ptr;
// specially decorated "pinned" IL local slot, not visible to user code.
pinned ref byte _pinned;
try
{
// NOTE: null check is omitted for value types
// NOTE: `thing` is evaluated only once (temporary is introduced if necessary)
if (thing != null)
{
// obtain and "pin" the reference
_pinned = ref thing.GetPinnableReference();
// unsafe cast in IL
ptr = (byte*)_pinned;
}
else
{
ptr = default(byte*);
}
// <BODY>
}
finally // finally can be omitted when not observable
{
// "unpin" the object
_pinned = nullptr;
}
Inconvenientes
GetPinnableReference está pensado para usarse solo en fixed , pero nada impide su uso en código seguro,
por lo que el implementador debe tenerlo en cuenta.
Alternativas
Los usuarios pueden introducir GetPinnableReference o un miembro similar y usarlo como
fixed(byte* ptr = thing.GetPinnableReference())
{
// <BODY>
}
Preguntas no resueltas
[] Comportamiento en estado "vacío". - nullptr``undefined ¿o?
[] ¿Deben tenerse en cuenta los métodos de extensión?
[] Si se detecta un patrón en System.String , ¿debe ganarse?
Design Meetings
Ninguno todavía.
Reasignación local de referencia
18/09/2021 • 2 minutes to read
En C# 7,3, se agrega compatibilidad para reenlazar el referente de una variable local de referencia o un
parámetro ref.
Agregamos lo siguiente al conjunto de assignment_operator s.
assignment_operator
: '=' 'ref'
;
Resumen
Permite usar la sintaxis del inicializador de matriz con stackalloc .
Motivación
Las matrices normales pueden inicializar sus elementos en el momento de su creación. Parece razonable
permitir eso en caso de que se stackalloc trate.
La pregunta de por qué no se permite esta sintaxis con se stackalloc produce con bastante frecuencia.
Consulte, por ejemplo, #1112
Diseño detallado
Las matrices ordinarias se pueden crear mediante la sintaxis siguiente:
new int[3]
new int[3] { 1, 2, 3 }
new int[] { 1, 2, 3 }
new[] { 1, 2, 3 }
La semántica de todos los casos es aproximadamente la misma que con las matrices.
Por ejemplo: en el último caso, el tipo de elemento se deduce del inicializador y debe ser un tipo "no
administrado".
Nota: la característica no depende de que el destino sea un Span<T> . Es igual que si fuera necesario T* , por lo
que no parece razonable que lo predicase en caso de que no lo sea Span<T> .
Traducción
La implementación de Naive podría simplemente inicializar la matriz justo después de la creación a través de
una serie de asignaciones de elementos.
Del mismo modo que con las matrices, puede ser posible y deseable detectar los casos en los que todos o la
mayoría de los elementos son tipos que pueden transferirse en exceso de bits y usar técnicas más eficaces
copiando el estado creado previamente de todos los elementos constantes.
Inconvenientes
Alternativas
Esta es una característica útil. Solo es posible hacer nada.
Preguntas no resueltas
Design Meetings
Ninguno todavía.
Atributos de Field-Targeted de propiedades
implementadas automáticamente
18/09/2021 • 3 minutes to read
Resumen
Esta característica intenta permitir a los desarrolladores aplicar atributos directamente a los campos de respaldo
de las propiedades implementadas automáticamente.
Motivación
Actualmente no es posible aplicar atributos a los campos de respaldo de las propiedades implementadas
automáticamente. En aquellos casos en los que el desarrollador debe utilizar un atributo de destino de campo, se
les obliga a declarar el campo manualmente y usar la sintaxis de propiedad más detallada. Dado que C# siempre
admitía atributos de destino de campo en el campo de respaldo generado para los eventos, tiene sentido
extender la misma funcionalidad a su más familiar.
Diseño detallado
En Resumen, las siguientes opciones serían de C# legal y no generarían una advertencia:
[Serializable]
public class Foo
{
[field: NonSerialized]
public string MySecret { get; set; }
}
Esto daría lugar a la aplicación de los atributos de destino de campo al campo de respaldo generado por el
compilador:
[Serializable]
public class Foo
{
[NonSerialized]
private string _mySecretBackingField;
Como se mencionó, esto proporciona la paridad con la sintaxis de eventos de C# 1,0, ya que lo siguiente es
válido y se comporta de la manera esperada:
[Serializable]
public class Foo
{
[field: NonSerialized]
public event EventHandler MyEvent;
}
Inconvenientes
Hay dos posibles inconvenientes en la implementación de este cambio:
1. Al intentar aplicar un atributo al campo de una propiedad implementada automáticamente, se genera una
advertencia del compilador que indica que los atributos de ese bloque se omitirán. Si el compilador se ha
cambiado para admitir esos atributos, se les aplicaría al campo de respaldo en una recompilación posterior
que podría modificar el comportamiento del programa en tiempo de ejecución.
2. El compilador no valida actualmente los destinos de AttributeUsage de los atributos al intentar aplicarlos al
campo de la propiedad implementada automáticamente. Si el compilador se ha cambiado para admitir
atributos dirigidos a campos y el atributo en cuestión no se puede aplicar a un campo, el compilador emitirá
un error en lugar de una advertencia, interrumpiendo la compilación.
Alternativas
Preguntas no resueltas
Design Meetings
Variables de expresión en inicializadores
18/09/2021 • 2 minutes to read
Resumen
Se amplían las características introducidas en C# 7 para permitir expresiones que contengan variables de
expresión (modelos de declaración y declaraciones de variables out) en inicializadores de campo, inicializadores
de propiedad, constructores de inicializadores y cláusulas de consulta.
Motivación
Esto completa un par de bordes aproximados que quedan en el lenguaje C# debido a la falta de tiempo.
Diseño detallado
Quitamos la restricción que impide la declaración de variables de expresión (declaraciones de variables de salida
y modelos de declaración) en un inicializador de constructor. Este tipo de variable declarado está en el ámbito de
todo el cuerpo del constructor.
Quitamos la restricción que impide la declaración de variables de expresión (declaraciones de variables y
modelos de declaración) en un inicializador de campo o propiedad. Este tipo de variable declarado está en el
ámbito de la expresión de inicialización.
Quitamos la restricción que impide la declaración de variables de expresión (declaraciones de variables de salida
y modelos de declaración) en una cláusula de expresión de consulta que se traduce en el cuerpo de una
expresión lambda. Este tipo de variable declarado está en el ámbito de esa expresión de la cláusula de consulta.
Inconvenientes
Ninguno.
Alternativas
El ámbito adecuado para las variables de expresión declaradas en estos contextos no es obvio y merece más
información sobre el LDM.
Preguntas no resueltas
[] ¿Cuál es el ámbito adecuado para estas variables?
Design Meetings
Ninguno.
Compatibilidad con = = y! = en tipos de tupla
18/09/2021 • 9 minutes to read
Permite expresiones t1 == t2 donde t1 y t2 son tipos de tupla que aceptan valores NULL de la misma
cardinalidad y los evalúan aproximadamente como temp1.Item1 == temp2.Item1 && temp1.Item2 == temp2.Item2
(suponiendo que var temp1 = t1; var temp2 = t2; ).
Por el contrario, permitiría t1 != t2 y evaluaría como
temp1.Item1 != temp2.Item1 || temp1.Item2 != temp2.Item2 .
En el caso que acepta valores NULL, se usan comprobaciones adicionales para temp1.HasValue y
temp2.HasValue . Por ejemplo, se nullableT1 == nullableT2 evalúa como
temp1.HasValue == temp2.HasValue ? (temp1.HasValue ? ... : true) : false .
Cuando una comparación de elementos devuelve un resultado que no es bool (por ejemplo, cuando se utiliza un
definido por el usuario operator == o que no operator != es bool, o en una comparación dinámica), ese
resultado se convertirá en bool o se ejecutará a través de operator true o operator false para obtener bool
. La comparación de tupla siempre termina con la devolución de un bool .
A partir de C# 7,2, este código genera un error (
error CS0019: Operator '==' cannot be applied to operands of type '(...)' and '(...)' ), a menos que haya un
definido por el usuario operator== .
Detalles
Al enlazar el == operador (o != ), las reglas existentes son: (1) caso dinámico, (2) resolución de sobrecarga y
(3) producen un error. Esta propuesta agrega un caso de tupla entre (1) y (2): si ambos operandos de un
operador de comparación son tuplas (tienen tipos de tupla o son literales de tupla) y tienen una cardinalidad
coincidente, la comparación se realiza a elemento. Esta igualdad de tupla también se eleva en tuplas que aceptan
valores NULL.
Ambos operandos (y, en el caso de los literales de tupla, sus elementos) se evalúan en orden de izquierda a
derecha. A continuación, cada par de elementos se utiliza como operandos para enlazar el operador == (o !=
), de forma recursiva. Cualquier elemento con el tipo en tiempo de compilación dynamic produce un error. Los
resultados de esas comparaciones de elementos se usan como operandos en una cadena de operadores
condicionales AND (or OR).
Por ejemplo, en el contexto de (int, (int, int)) t1, t2; , se t1 == (1, (2, 3)) evaluaría como
temp1.Item1 == temp2.Item1 && temp1.Item2.Item1 == temp2.Item2.Item1 && temp1.Item2.Item2 ==
temp2.Item2.Item2
.
Cuando se usa un literal de tupla como operando (en cualquier lado), recibe un tipo de tupla convertido
formado por las conversiones de elementos que se introducen al enlazar el operador == (o != ) por elemento.
Por ejemplo, en (1L, 2, "hello") == (1, 2L, null) , el tipo convertido para ambos literales de tupla es
(long, long, string) y el segundo literal no tiene ningún tipo natural.
Desconstrucción y conversiones a tupla
En (a, b) == x , el hecho de que se x puede deconstruir en dos elementos no desempeña un rol. Esto podría
estar en una propuesta futura, aunque podría plantear preguntas acerca de x == y (es una comparación simple
o una comparación de elementos y, si así se usa la cardinalidad?). Del mismo modo, las conversiones a tupla no
juegan ningún rol.
Nombres de elementos de tupla
Al convertir un literal de tupla, se advierte cuando se proporciona un nombre de elemento de tupla explícito en
el literal, pero no coincide con el nombre de elemento de la tupla de destino. Usamos la misma regla en la
comparación de tuplas, por lo que (int a, int b) t supondremos que se advierte de d en t == (c, d: 0) .
Resultados de la comparación de elementos no booleanos
Si una comparación de elementos es dinámica en una igualdad de tupla, usamos una invocación dinámica del
operador false y niega eso para obtener bool y continuar con comparaciones de elementos adicionales.
Si una comparación de elementos devuelve algún otro tipo que no sea bool en una igualdad de tupla, hay dos
casos:
Si el tipo que no es bool se convierte en bool , se aplica esa conversión.
Si no hay tal conversión, pero el tipo tiene un operador false , lo usaremos y negará el resultado.
En una desigualdad de tupla, se aplican las mismas reglas excepto que usaremos el operador true (sin
negación) en lugar del operador false .
Estas reglas son similares a las reglas implicadas para usar un tipo que no sea bool en una if instrucción y
otros contextos existentes.
new A(1)
new B(2)
new B(3)
new B(4)
GetTuple()
a continuación, se evalúan las conversiones y comparaciones de elementos y la lógica condicional (convertir
new A(1) al tipo B , compararlo con new B(4) , etc.).
Este es un caso especial de las comparaciones normales, que lleva a las comparaciones de tupla. La
null == null comparación se permite y los null literales no obtienen ningún tipo. En la igualdad de tupla,
esto significa (0, null) == (0, null) que también se permite y los null literales de tupla y no obtienen un
tipo.
Comparar un struct que acepta valores NULL con null sin operator==
Este es otro caso especial de las comparaciones normales, que lleva a las comparaciones de tupla. Si tiene un
struct S sin operator== , (S?)x == null se permite la comparación y se interpreta como ((S?).x).HasValue .
En la igualdad de tupla, se aplica la misma regla, por lo que (0, (S?)x) == (0, null) se permite.
Compatibilidad
Si alguien escribe sus propios ValueTuple tipos con una implementación del operador de comparación, se
habrían elegido previamente por la resolución de sobrecarga. Pero como el nuevo caso de tupla viene antes de
la resolución de sobrecarga, se trataría este caso con la comparación de tupla en lugar de depender de la
comparación definida por el usuario.
Está relacionado con los operadores relacionales y de prueba de tipos relacionados con #190
Mejoras en los candidatos de sobrecarga
18/09/2021 • 2 minutes to read
Resumen
Las reglas de resolución de sobrecarga se han actualizado en casi todas las actualizaciones del lenguaje C# para
mejorar la experiencia de los programadores, haciendo que las invocaciones ambiguas seleccionen la opción
"obvio". Esto se debe hacer con cuidado para mantener la compatibilidad con versiones anteriores, pero dado
que normalmente estamos resolviendo lo que, de otro modo, serían casos de error, estas mejoras normalmente
funcionan bien.
1. Cuando un grupo de métodos contiene miembros estáticos y de instancia, se descartan los miembros de
instancia si se invocan sin un receptor o contexto de instancia y se descartan los miembros estáticos si se
invocan con un receptor de instancia. Cuando no hay ningún receptor, se incluyen solo los miembros
estáticos en un contexto estático, de lo contrario, los miembros estáticos y de instancia. Cuando el receptor es
ambiguomente una instancia o un tipo debido a una situación de color del color, incluimos ambos. Un
contexto estático, donde no se puede usar un receptor implícito de esta instancia, incluye el cuerpo de los
miembros en los que no está definido, como los miembros estáticos, así como los lugares en los que no se
puede usar, como los inicializadores de campo y los inicializadores de constructor.
2. Cuando un grupo de métodos contiene algunos métodos genéricos cuyos argumentos de tipo no cumplen
con sus restricciones, estos miembros se eliminan del conjunto de candidatos.
3. En el caso de una conversión de grupo de métodos, los métodos candidatos cuyo tipo de valor devuelto no
coincide con el tipo de valor devuelto del delegado se eliminan del conjunto.
Tipos de referencia que aceptan valores NULL en C
#
18/09/2021 • 14 minutes to read
Expresión de intención
El lenguaje ya contiene la T? Sintaxis de los tipos de valor. Es sencillo extender esta sintaxis a tipos de
referencia.
Se supone que la intención de un tipo de referencia no adornada T es que no sea NULL.
C<T?> Si el parámetro de tipo tiene restricciones que no son NULL, se proporciona una advertencia.
Genéricos
Si un parámetro de tipo T tiene restricciones que no aceptan valores NULL, se trata como un valor que no
acepta valores NULL dentro de su ámbito.
Si un parámetro de tipo no está restringido o solo tiene restricciones que aceptan valores NULL, la situación es
un poco más compleja: Esto significa que el argumento de tipo correspondiente podría admitir valores NULL o
que no admita valores NULL. Lo más seguro que hacer en esa situación es tratar el parámetro de tipo como
valores NULL y que no aceptan valores NULL, lo que permite recibir advertencias cuando se infringe cualquiera
de ellos.
Merece la pena considerar si se deben permitir las restricciones de referencia explícitas que aceptan valores
NULL. Tenga en cuenta, sin embargo, que no se puede evitar que los tipos de referencia que aceptan valores
NULL sean restricciones de forma implícita en ciertos casos (restricciones heredadas).
La class restricción no es NULL. Podemos considerar si class? debe ser una restricción válida que acepte
valores NULL que indique "tipo de referencia que acepta valores NULL".
Inferencia de tipos
En la inferencia de tipos, si un tipo de contribución es un tipo de referencia que acepta valores NULL, el tipo
resultante debe admitir valores NULL. En otras palabras, se propaga la nulación.
Debemos considerar si el null literal como una expresión participante debe contribuir a la nulación. En la
actualidad: para los tipos de valor, conduce a un error, mientras que en los tipos de referencia el valor NULL se
convierte correctamente en el tipo simple.
string? n = "world";
var x = b ? "Hello" : n; // string?
var y = b ? "Hello" : null; // string? or error
var z = b ? 7 : null; // Error today, could be int?
En el ejemplo anterior, la DoWork función acepta un Worker y evita que sea posible null . Si el worker
argumento es null , la DoWork función será throw . Con tipos de referencia que aceptan valores NULL, el
código del ejemplo anterior hace que el Worker parámetro no sea null . Si la DoWork función era una API
pública, como un paquete NuGet o una biblioteca compartida, como guía, debe dejar las protecciones nulas.
Como una API pública, la única garantía de que el autor de la llamada no null está pasando es protegerse
frente a ella.
Intención rápida
Un uso más atractivo del ejemplo anterior es expresar que el Worker parámetro podría ser null , por lo que la
protección nula es más adecuada. Si quita la protección nula en el ejemplo siguiente, el compilador advierte que
se puede desreferenciar null. Sin embargo, ambas protecciones nulas siguen siendo válidas.
En el caso de las API no públicas, como el código fuente por completo en el control por parte de un
desarrollador o un equipo de desarrollo, los tipos de referencia que aceptan valores NULL podrían permitir la
eliminación segura de los resguardos nulos en los que los desarrolladores pueden garantizar que no son
necesarios. La característica puede ayudar con las advertencias, pero no puede garantizar que en la ejecución
del código en tiempo de ejecución podría dar como resultado una NullReferenceException .
Últimos cambios
Las advertencias que no son NULL son un cambio importante evidente en el código existente y deben ir
acompañados de un mecanismo de participación.
Menos obvio, las advertencias de tipos que aceptan valores NULL (como se describió anteriormente) son un
cambio importante en el código existente en ciertos escenarios en los que la nulabilidad es implícita:
Los parámetros de tipo sin restricciones se tratarán como Nullable implícitamente, por lo que asignarlos a
object o tener acceso a, por ejemplo, ToString producirá advertencias.
Si la inferencia de tipos infiere la nulación de las null expresiones, el código existente producirá en
ocasiones valores NULL en lugar de tipos que no aceptan valores NULL, lo que puede dar lugar a nuevas
advertencias.
Por lo tanto, las advertencias que aceptan valores null también deben ser opcionales
Por último, la adición de anotaciones a una API existente será un cambio importante para los usuarios que
hayan participado en las advertencias al actualizar la biblioteca. Esto también merece la posibilidad de participar
o no. "Quiero correcciones de errores, pero no estoy listo para tratar con sus nuevas anotaciones"
En Resumen, debe ser capaz de participar o no de:
Advertencias que aceptan valores NULL
Advertencias no nulas
Advertencias de anotaciones en otros archivos
La granularidad de la participación sugiere un modelo similar a un analizador, en el que el usuario puede elegir
y deshabilitar swaths de código con las directivas pragma y los niveles de gravedad. Además, las opciones por
biblioteca ("omitir las anotaciones de JSON.NET hasta que esté listo para tratar con el descenso") pueden
expresarse en el código como atributos.
El diseño de la experiencia de participación o transición es fundamental para el éxito y la utilidad de esta
característica. Debemos asegurarnos de que:
Los usuarios pueden adoptar la comprobación de la nulabilidad gradualmente como deseen
Los autores de bibliotecas pueden agregar anotaciones de nulabilidad sin miedo a los clientes de la
interrupción
A pesar de esto, no hay una idea de la "pesadilla de configuración".
Ajustes
Podríamos considerar no usar las ? anotaciones en variables locales, pero solo observa si se usan de acuerdo
con lo que se les asigna. No me gusta esto; Creo que deberíamos permitir que los usuarios expresen su
intención.
Podríamos considerar una abreviatura T! x en los parámetros, que genera automáticamente una
comprobación de NULL en tiempo de ejecución.
Algunos patrones de tipos genéricos, como FirstOrDefault o TryGet , tienen un comportamiento ligeramente
extraño con argumentos de tipo que no aceptan valores NULL, ya que generan explícitamente valores
predeterminados en determinadas situaciones. Podríamos intentar maticar el sistema de tipos para
acomodarlos mejor. Por ejemplo, podríamos permitir ? en parámetros de tipo sin restricciones, aunque el
argumento de tipo ya podría aceptar valores NULL. No dude en que merezca la pena, y conduce a la extrañaidad
relacionada con la interacción con tipos de valor que aceptan valores NULL.
Sintaxis
Tipos de referencia que aceptan valores NULL y parámetros de tipo que aceptan valores NULL
Los tipos de referencia que aceptan valores NULL y los parámetros de tipo que aceptan valores NULL tienen la
misma sintaxis T? que la forma abreviada de tipos de valor que aceptan valores NULL, pero no tienen una
forma larga correspondiente.
Para los fines de la especificación, nullable_type se cambia el nombre de la producción actual a y
nullable_value_type nullable_reference_type se agregan las producciones:
nullable_type_parameter
type
: value_type
| reference_type
| nullable_type_parameter
| type_parameter
| type_unsafe
;
reference_type
: ...
| nullable_reference_type
;
nullable_reference_type
: non_nullable_reference_type '?'
;
non_nullable_reference_type
: reference_type
;
nullable_type_parameter
: non_nullable_non_value_type_parameter '?'
;
non_nullable_non_value_type_parameter
: type_parameter
;
primary_constraint
: ...
| 'class' '?'
;
Se debe crear una instancia de un parámetro de tipo restringido con class (en un contexto de anotación
habilitado ) con un tipo de referencia que no acepte valores NULL.
Se puede crear una instancia de un parámetro de tipo restringido con class? (o class en un contexto de
anotación deshabilitado ) con un tipo de referencia que acepte valores NULL o que no acepte valores NULL.
Se proporciona una advertencia en una class? restricción en un contexto de anotación deshabilitado .
notnull restricción
Un parámetro de tipo restringido con notnull no puede ser un tipo que acepta valores NULL (tipo de valor que
acepta valores NULL, tipo de referencia que acepta valores NULL o parámetro de tipo que acepta valores NULL).
primary_constraint
: ...
| 'notnull'
;
default restricción
La default restricción se puede usar en una invalidación de método o en una implementación explícita para
eliminar la ambigüedad que T? significa "parámetro de tipo que acepta valores NULL" de "tipo de valor que
acepta valores NULL" ( Nullable<T> ). Si falta la default restricción T? , una sintaxis en una implementación
de invalidación o explícita se interpretará como Nullable<T>
Consulta https://github.com/dotnet/csharplang/blob/master/proposals/csharp-9.0/unconstrained-type-
parameter-annotations.md#default-constraint.
El operador null-permisivo
El operador posterior a la corrección ! se denomina operador permisivo nulo. Se puede aplicar en un
primary_expression o en un null_conditional_expression:
primary_expression
: ...
| null_forgiving_expression
;
null_forgiving_expression
: primary_expression '!'
;
null_conditional_expression
: primary_expression null_conditional_operations_no_suppression suppression?
;
null_conditional_operations_no_suppression
: null_conditional_operations? '?' '.' identifier type_argument_list?
| null_conditional_operations? '?' '[' argument_list ']'
| null_conditional_operations '.' identifier type_argument_list?
| null_conditional_operations '[' argument_list ']'
| null_conditional_operations '(' argument_list? ')'
;
null_conditional_operations
: null_conditional_operations_no_suppression suppression?
;
suppression
: '!'
;
Por ejemplo:
var v = expr!;
expr!.M();
_ = a?.b!.c;
pp_nullable
: whitespace? '#' whitespace? 'nullable' whitespace nullable_action (whitespace nullable_target)?
pp_new_line
;
nullable_action
: 'disable'
| 'enable'
| 'restore'
;
nullable_target
: 'warnings'
| 'annotations'
;
#pragma warning las directivas se expanden para permitir cambiar el contexto de advertencia que acepta valores
NULL:
pragma_warning_body
: ...
| 'warning' whitespace warning_action whitespace 'nullable'
;
Por ejemplo:
Nulabilidad de tipos
Un tipo determinado puede tener uno de tres nullabilities: desconocen, nonnullable y Nullable.
Los tipos que no aceptan valores NULL pueden producir advertencias si null se le asigna un valor potencial.
Sin embargo, los tipos desconocen y que aceptan valores NULL son "asignable null" y pueden tener null
valores asignados sin advertencias.
Los valores de desconocen y los tipos que no aceptan valores NULL se pueden desreferenciar o asignar sin
advertencias. Sin embargo, los valores de los tipos que aceptan valores NULL son "produjeron valores NULL" y
pueden provocar advertencias al desreferenciarse o asignarse sin una comprobación nula adecuada.
El estado predeterminado NULL de un tipo de rendimiento nulo es "quizás null" o "quizás default". El estado
Null predeterminado de un tipo de rendimiento que no es NULL es "not null".
El tipo de tipo y el contexto de anotación que acepta valores NULL que tiene lugar en determina su nulabilidad:
Un tipo de valor que no acepta valores NULL S siempre es no acepta valores NULL
Un tipo de valor que acepta valores NULL S? siempre admite valores NULL
Un tipo de referencia sin anotar C en un contexto de anotación deshabilitado es desconocen
Un tipo de referencia sin anotar C en un contexto de anotación habilitado no admite valores NULL
Un tipo de referencia que acepta valores NULL admite C? valores NULL (pero se puede producir una
advertencia en un contexto de anotación deshabilitado )
Los parámetros de tipo también tienen en cuenta las restricciones:
Parámetro de tipo T en el que todas las restricciones (si existen) son tipos que aceptan valores NULL o la
class? restricción admite valores NULL .
Un parámetro de tipo en T el que al menos una restricción es desconocen o que no admite valores NULL , o
una de las struct class restricciones o notnull es.
desconocen en un contexto de anotación deshabilitado
no acepta valores NULL en un contexto de anotación habilitado
Un parámetro de tipo que acepta valores NULL admite T? valores NULL, pero se produce una advertencia
en un contexto de anotación deshabilitado si T no es un tipo de valor
Desconocen frente a no Nullable
type Se considera que se produce en un contexto de anotación determinado cuando el último token del tipo
está dentro de ese contexto.
Si un tipo de referencia determinado C en el código fuente se interpreta como desconocen o no acepta valores
NULL, depende del contexto de la anotación de ese código fuente. Pero una vez establecido, se considera parte
de ese tipo y "viaja con él", por ejemplo, durante la sustitución de los argumentos de tipo genérico. Es como si
hubiera una anotación como ? en el tipo, pero no es visible.
Restricciones
Los tipos de referencia que aceptan valores NULL se pueden usar como restricciones genéricas.
class? es una nueva restricción que denota "posible tipo de referencia que acepta valores NULL", mientras que
class en un contexto de anotación habilitado denota "tipo de referencia que no acepta valores NULL".
default es una nueva restricción que denota un parámetro de tipo que no se sabe que es un tipo de referencia
o de valor. Solo se puede usar en métodos invalidados y implementados explícitamente. Con esta restricción,
T? significa un parámetro de tipo que acepta valores NULL, en lugar de ser un método abreviado para
Nullable<T> .
notnull es una nueva restricción que denota un parámetro de tipo que no acepta valores NULL.
La nulabilidad de un argumento de tipo o de una restricción no afecta a si el tipo satisface la restricción, excepto
en el caso de que ya sea el caso hoy (los tipos de valor que aceptan valores NULL no satisfacen la struct
restricción). Sin embargo, si el argumento de tipo no satisface los requisitos de nulabilidad de la restricción, se
puede proporcionar una advertencia.
// The value `t` here has the state "maybe null". It's possible for `T` to be instantiated
// with `string?` in which case `null` would be within the domain of legal values here. The
// assumption though is the value provided here is within the legal values of `T`. Hence
// if `T` is `string` then `null` will not be a value, just as we assume that `null` is not
// provided for a normal `string` parameter
void M<T>(T t)
{
// There is no guarantee that default(T) is within the legal values for T hence the
// state *must* be "maybe-default" and hence `local` must be `T?`
T? local = default(T);
}
void Use(string s)
{
// ...
}
Expresiones de invocación
Si invocation_expression invoca un miembro declarado con uno o varios atributos para un comportamiento
especial nulo, el estado NULL se determina mediante esos atributos. De lo contrario, el estado null de la
expresión es el estado Null predeterminado de su tipo.
invocation_expression El compilador no realiza el seguimiento del estado null de un.
Acceso a elementos
Si element_access invoca un indexador que se declara con uno o más atributos para un comportamiento
especial nulo, los atributos determinan el estado null. De lo contrario, el estado null de la expresión es el estado
Null predeterminado de su tipo.
object?[] array = ...;
if (array[0] != null)
{
// Warning: Converting null literal or possible null value to non-nullable type.
object o = array[0];
// Warning: Dereference of a possibly null reference.
Console.WriteLine(o.ToString());
}
Acceso base
Si B denota el tipo base del tipo envolvente, base.I tiene el mismo estado null que ((B)this).I y base[E]
tiene el mismo estado null que ((B)this)[E] .
Expresiones predeterminadas
default(T) tiene el estado null en función de las propiedades del tipo T :
Si el tipo es un tipo que no acepta valores NULL , tiene el estado null "not null"
De lo contrario, si el tipo es un parámetro de tipo, tiene el estado null "quizás default".
En caso contrario, tiene el estado null "quizás null"
Expresiones condicionales nulas?.
Un null_conditional_expression tiene el estado Null basado en el tipo de expresión. Tenga en cuenta que esto
hace referencia al tipo de null_conditional_expression , no al tipo original del miembro que se invoca:
Si el tipo es un tipo de valor que acepta valores NULL , tiene el estado null "maybe null"
De lo contrario, si el tipo es un parámetro de tipo que acepta valores NULL , tiene el estado null "quizás
default".
En caso contrario, tiene el estado null "quizás null"
Expresiones de conversión
Si una expresión de conversión (T)E invoca una conversión definida por el usuario, el estado null de la
expresión es el estado predeterminado NULL para el tipo de la conversión definida por el usuario. De lo
contrario:
Si Tes un tipo de valor que no acepta valores NULL , T tiene el estado null "not null"
T De lo contrario, si es un tipo de valor que acepta valores NULL , T tiene el estado null "maybe null"
De lo contrario T , si es un tipo que acepta valores NULL en el formulario U? U , donde es un parámetro
de tipo, T tiene el estado null "quizás default".
De lo contrario, si T es un tipo que acepta valores NULL y E tiene el estado null "maybe null" o "quizás
default", T tiene el estado null "maybe null"
De lo contrario, si T es un parámetro de tipo y E tiene el estado null "maybe null" o "quizás default", T
tiene el estado null "quizás default".
Else T tiene el mismo estado null que E
Operadores unarios y binarios
Si un operador unario o binario invoca a un operador definido por el usuario, el estado null de la expresión es el
estado predeterminado NULL para el tipo del operador definido por el usuario. De lo contrario, es el estado null
de la expresión.
¿Algo especial de hacer para binario + en cadenas y delegados?
Expresiones Await
El estado null de await E es el estado Null predeterminado de su tipo.
El operador as
El estado null de una E as T expresión depende primero de las propiedades del tipo T . Si el tipo de T no
admite valores NULL , el estado NULL es "not null". En caso contrario, el estado Null depende de la conversión
del tipo de E al tipo T :
Si la conversión es una identidad, una conversión boxing, una referencia implícita o una conversión implícita
que acepta valores NULL, el estado NULL es el estado null de E
T De lo contrario, si es un parámetro de tipo, tiene el estado null "quizás default".
En caso contrario, tiene el estado null "quizás null"
Operador de uso combinado de null
El estado null de E1 ?? E2 es el estado nulo de E2
El operador condicional
El estado null de E1 ? E2 : E3 se basa en el estado null de E2 y E3 :
Si ambos son "not null", el estado NULL es "not null"
De lo contrario, si es "quizás default", el estado NULL es "quizás default".
De lo contrario, el estado NULL es "not null"
Expresiones de consulta
El estado null de una expresión de consulta es el estado Null predeterminado de su tipo.
Trabajo adicional necesario aquí
Operadores de asignación
E1 = E2 y E1 op= E2 tienen el mismo estado null que E2 cuando se ha aplicado cualquier conversión
implícita.
Expresiones que propagan el estado null
(E)``checked(E) y unchecked(E) todos tienen el mismo estado null que E .
Expresiones que nunca son NULL
El estado null de los siguientes formatos de expresión siempre es "not null":
this access
cadenas interpoladas
new expresiones (expresiones de objeto, delegado, objeto anónimo y creación de matriz)
Expresiones typeof
Expresiones nameof
funciones anónimas (métodos anónimos y expresiones lambda)
Expresiones permisivo nulas
Expresiones is
Funciones anidadas
Las funciones anidadas (expresiones lambda y funciones locales) se tratan como métodos, excepto en lo que
respecta a las variables capturadas. El estado inicial de una variable capturada dentro de una función lambda o
local es la intersección del estado que acepta valores NULL de la variable en todos los "usos" de esa función
anidada o expresión lambda. El uso de una función local es una llamada a esa función, o bien, donde se
convierte en un delegado. El uso de una expresión lambda es el punto en el que se define en el origen.
Inferencia de tipos
variables locales con tipo implícito que aceptan valores NULL
var deduce un tipo anotado para los tipos de referencia y los parámetros de tipo que no están restringidos
para ser un tipo de valor. Por ejemplo:
en var s = ""; var , se deduce como string? .
en var t = new T(); con una sin restricciones T var , se deduce como T? .
Inferencia de tipo genérico
La inferencia de tipos genéricos se ha mejorado para ayudar a decidir si los tipos de referencia deducidos deben
admitir valores NULL o no. Se trata de un mejor esfuerzo. Puede producir advertencias con respecto a las
restricciones de nulabilidad y puede dar lugar a advertencias que aceptan valores NULL cuando los tipos
deducidos de la sobrecarga seleccionada se aplican a los argumentos.
La primera fase
Los tipos de referencia que aceptan valores NULL fluyen en los límites de las expresiones iniciales, como se
describe a continuación. Además, null se introducen dos nuevos tipos de límites, es decir, y default . Su
finalidad es llevar a cabo a través de null las apariciones de o default en las expresiones de entrada, lo que
puede hacer que un tipo deducido acepte valores NULL, aunque no lo sea. Esto funciona incluso para los tipos
de valor que aceptan valores NULL, que se han mejorado para recoger la "nulación" en el proceso de inferencia.
La determinación de los límites que se van a agregar en la primera fase se ha mejorado como se indica a
continuación:
Si un argumento Ei tiene un tipo de referencia, el tipo que se U usa para la inferencia depende del estado null
de, así Ei como de su tipo declarado:
Si el tipo declarado es un tipo de referencia que no admite valores NULL U0 o un tipo de referencia que
acepta valores NULL U0? , entonces
Si el estado null de Ei es "not null", entonces U``U0
Si el estado null de Ei es "quizás null", entonces U``U0?
De lo contrario Ei , si tiene un tipo declarado, U es de ese tipo.
De lo Ei contrario null , si es, U el enlace especial null
De lo Ei contrario default , si es, U el enlace especial default
En caso contrario, no se realiza ninguna inferencia.
Inferencias exactas, de límite superior y de límite inferior
En las inferencias del tipo U al tipo V , si V es un tipo de referencia que acepta valores NULL V0? , V0 se
usa en lugar de V en las cláusulas siguientes.
Si V es una de las variables de tipo sin corregir, U se agrega como un límite inferior, superior o inferior
como antes.
De lo contrario, si U es null o default , no se realiza ninguna inferencia.
De lo contrario, si U es un tipo de referencia que acepta valores NULL U0? , U0 se utiliza en lugar de U en
las cláusulas posteriores.
La esencia es que la nulabilidad que pertenece directamente a una de las variables de tipo sin Fixed se conserva
en sus límites. Por otra parte, para las inferencias que se recorren más en los tipos de origen y de destino, se
omite la nulabilidad. Puede o no coincidir, pero si no es así, se emitirá una advertencia más adelante si se elige la
sobrecarga y se aplica.
Corrección de
Actualmente, la especificación no realiza un buen trabajo de describir lo que sucede cuando varios límites son
convertibles entre sí, pero son diferentes. Esto puede ocurrir entre object y dynamic , entre tipos de tupla que
solo difieren en los nombres de elemento, entre los tipos construidos en él y ahora también entre C y C? para
los tipos de referencia.
Además, debemos propagar la "nulación" de las expresiones de entrada al tipo de resultado.
Para controlar estos agregamos más fases para corregir, que ahora es:
1. Recopile todos los tipos de todos los límites como candidatos, quitando ? de todos los tipos de referencia
que aceptan valores NULL
2. Eliminación de candidatos en función de los requisitos de los límites exactos, inferiores y superiores
(mantenimiento null y default límites)
3. Eliminación de candidatos que no tienen una conversión implícita a todos los demás candidatos
4. Si los candidatos restantes no tienen conversiones de identidad entre sí, se produce un error en la inferencia
de tipos
5. Combine los candidatos restantes como se describe a continuación
6. Si el candidato resultante es un tipo de referencia o un tipo de valor que no acepta valores NULL y todos los
límites exactos o alguno de los límites inferiores son tipos de valor que aceptan valores NULL, tipos de
referencia que aceptan valores NULL o null default , ? se agregan al candidato resultante, lo que lo
convierte en un tipo de valor que acepta valores NULL o un tipo de referencia.
La combinación se describe entre dos tipos candidatos. Es transitiva y conmutable, por lo que los candidatos se
pueden combinar en cualquier orden con el mismo resultado final. No se define si los dos tipos candidatos no
son una identidad convertible entre sí.
La función Merge toma dos tipos de candidatos y una dirección ( + o - ):
Merge( T , , d) = T
T
Merge( S T? , + ) = Merge( S? , T , + ) = Merge( S , T , + ) ?
,
Merge( S T? , - ) = Merge( S? , T , - ) = Merge( S , T , - )
,
Merge( C<S1,...,Sn> , C<T1,...,Tn> , + ) = C< Merge( S1 , T1 , D1 ) ,..., Merge( Sn , Tn , DN) > ,
donde
di = + Si el i parámetro de tipo TH de C<...> es covariante
di = - Si el i parámetro de tipo ' th of C<...> es compensa-or invariable
Merge( C<S1,...,Sn> , C<T1,...,Tn> , - ) = C< Merge( S1 , T1 , D1) ,..., Merge( Sn , Tn , DN) > ,
donde
di = - Si el i parámetro de tipo TH de C<...> es covariante
di = + Si el i parámetro de tipo ' th of C<...> es compensa-or invariable
Merge( (S1 s1,..., Sn sn) , (T1 t1,..., Tn tn) , d) = ( Merge( S1 , T1 , d) n1,..., Merge( Sn , Tn ,
d) nn) , donde
ni está ausente si si y ti difieren, o si ambos están ausentes
ni es si si si y ti son iguales.
Merge( object , dynamic ) = Merge( dynamic , object ) = dynamic
Advertencias
Posible asignación null
Desreferenciación potencial null
No coincide la nulabilidad de restricciones
Tipos que aceptan valores NULL en el contexto de anotación deshabilitado
Incoherencia en la nulabilidad de invalidación e implementación
Resumen
Las extensiones de coincidencia de patrones para C# permiten muchas de las ventajas de los tipos de datos
algebraicos y la coincidencia de patrones de los lenguajes funcionales, pero de una manera que se integra sin
problemas con la sensación del lenguaje subyacente. Los elementos de este enfoque están inspirados en las
características relacionadas en los lenguajes de programación F # y Scala.
Diseño detallado
Expresión is
El is operador se extiende para probar una expresión con un patrón.
relational_expression
: is_pattern_expression
;
is_pattern_expression
: relational_expression 'is' pattern
;
Esta forma de relational_expression se suma a los formularios existentes en la especificación de C#. Es un error
en tiempo de compilación si el relational_expression a la izquierda del is token no designa un valor o no tiene
un tipo.
Cada identificador del patrón introduce una nueva variable local que se asigna definitivamente después de que
el is operador sea true (es decir, se asigna definitivamente cuando es true).
Nota: técnicamente existe una ambigüedad entre el tipo de una is-expression constant_pattern y,
cualquiera de los cuales puede ser un análisis válido de un identificador calificado. Intentamos enlazarlo
como un tipo para la compatibilidad con versiones anteriores del lenguaje; solo si se produce un error, lo
resolvemos como hacemos una expresión en otros contextos, lo primero que se encontró (que debe ser una
constante o un tipo). Esta ambigüedad solo está presente en la parte derecha de una is expresión.
Patrones
Los patrones se usan en el operador is_pattern , en un switch_statement y en un switch_expression para
expresar la forma de los datos en los que se van a comparar los datos entrantes (que llamamos al valor de
entrada). Los patrones pueden ser recursivos para que se puedan comparar partes de los datos con
subpatrones.
pattern
: declaration_pattern
| constant_pattern
| var_pattern
| positional_pattern
| property_pattern
| discard_pattern
;
declaration_pattern
: type simple_designation
;
constant_pattern
: constant_expression
;
var_pattern
: 'var' designation
;
positional_pattern
: type? '(' subpatterns? ')' property_subpattern? simple_designation?
;
subpatterns
: subpattern
| subpattern ',' subpatterns
;
subpattern
: pattern
| identifier ':' pattern
;
property_subpattern
: '{' '}'
| '{' subpatterns ','? '}'
;
property_pattern
: type? property_subpattern simple_designation?
;
simple_designation
: single_variable_designation
| discard_designation
;
discard_pattern
: '_'
;
Modelo de declaración
declaration_pattern
: type simple_designation
;
El declaration_pattern ambos comprueba que una expresión es de un tipo determinado y la convierte a ese tipo
si la prueba se realiza correctamente. Esto puede introducir una variable local del tipo especificado denominado
por el identificador dado, si la designación es una single_variable_designation. Esa variable local se asigna
definitivamente cuando el resultado de la operación de coincidencia de patrones es true .
La semántica en tiempo de ejecución de esta expresión es que prueba el tipo en tiempo de ejecución del
operando del lado izquierdo relational_expression en el tipo del patrón. Si es de ese tipo en tiempo de ejecución
(o algún subtipo) y no null , el resultado de is operator es true .
Ciertas combinaciones de tipo estático del lado izquierdo y del tipo especificado se consideran incompatibles y
producen un error en tiempo de compilación. Se dice que un valor de tipo estático E es compatible con el
patrón con un tipo T si existe una conversión de identidad, una conversión de referencia implícita, una
conversión boxing, una conversión de referencia explícita o una conversión unboxing de E a T , o si uno de
esos tipos es un tipo abierto. Es un error en tiempo de compilación si una entrada de tipo E no es compatible
con el patrón con el tipo de un patrón de tipo con el que se encuentra una coincidencia.
El patrón de tipo es útil para realizar pruebas de tipo en tiempo de ejecución de tipos de referencia y reemplaza
la expresión
int? x = 3;
if (x is int v) { // code using v
constant_pattern
: constant_expression
;
Un patrón de constante prueba el valor de una expresión con respecto a un valor constante. La constante puede
ser cualquier expresión constante, como un literal, el nombre de una variable declarada const o una constante
de enumeración. Cuando el valor de entrada no es un tipo abierto, la expresión constante se convierte
implícitamente al tipo de la expresión coincidente; Si el tipo del valor de entrada no es compatible con el patrón
con el tipo de la expresión constante, la operación de coincidencia de patrones es un error.
El patrón c se considera que coincide con el valor de entrada convertido e si object.Equals(c, e) devolvería
true .
Esperamos ver e is null como la forma más común de probar null en el código recién escrito, ya que no
puede invocar un definido por el usuario operator== .
Patrón var
var_pattern
: 'var' designation
;
designation
: simple_designation
| tuple_designation
;
simple_designation
: single_variable_designation
| discard_designation
;
single_variable_designation
: identifier
;
discard_designation
: _
;
tuple_designation
: '(' designations? ')'
;
designations
: designation
| designations ',' designation
;
Si la designación es una simple_designation, una expresión e coincide con el patrón. En otras palabras, una
coincidencia con un patrón var siempre se realiza correctamente con una simple_designation. Si el
simple_designation es un single_variable_designation, el valor de e se enlaza a una variable local recién
introducida. El tipo de la variable local es el tipo estático de e.
Si la designación es una tuple_designation, el patrón es equivalente a un positional_pattern de la (var
designación del formulario,..., ) donde las Design s se encuentran en el tuple_designation. Por ejemplo, el
patrón var (x, (y, z)) es equivalente a (var x, (var y, var z)) .
Es un error si el nombre se var enlaza a un tipo.
Patrón de descartar
discard_pattern
: '_'
;
Una expresión e coincide siempre con el patrón _ . En otras palabras, cada expresión coincide con el patrón de
descarte.
Un patrón de descarte no se puede usar como el patrón de una is_pattern_expression.
Patrón posicional
Un patrón posicional comprueba que el valor de entrada no es null , invoca un Deconstruct método adecuado
y realiza una mayor coincidencia de patrones en los valores resultantes. También admite una sintaxis de patrón
similar a una tupla (sin el tipo que se proporciona) cuando el tipo del valor de entrada es el mismo que el tipo
que contiene Deconstruct , o si el tipo del valor de entrada es un tipo de tupla, o si el tipo del valor de entrada
es object o ITuple y el tipo en tiempo de ejecución de la expresión implementa ITuple .
positional_pattern
: type? '(' subpatterns? ')' property_subpattern? simple_designation?
;
subpatterns
: subpattern
| subpattern ',' subpatterns
;
subpattern
: pattern
| identifier ':' pattern
;
Patrón de propiedad
Un patrón de propiedades comprueba que el valor de entrada no null coincide de forma recursiva con los
valores extraídos por el uso de campos o propiedades accesibles.
property_pattern
: type? property_subpattern simple_designation?
;
property_subpattern
: '{' '}'
| '{' subpatterns ','? '}'
;
Es un error si algún subpatrón de una property_pattern no contiene un identificador (debe ser de la segunda
forma, que tiene un identificador). Una coma final después del último subpatrón es opcional.
Tenga en cuenta que un patrón de comprobación de valores NULL cae de un patrón de propiedad trivial. Para
comprobar si la cadena s no es null, puede escribir cualquiera de las siguientes formas:
Dada una coincidencia de una expresión e con el tipo de patrón { property_pattern_list } , se trata de un error
en tiempo de compilación si la expresión e no es compatible con el patrón con el tipo T designado por el tipo. Si
el tipo está ausente, se toma como el tipo estático de e. Si el identificador está presente, declara una variable de
patrón de tipo Type. Cada uno de los identificadores que aparecen en el lado izquierdo de su
property_pattern_list debe designar una propiedad o campo legible accesible de T. Si el simple_designation del
property_pattern está presente, define una variable de patrón de tipo T.
En tiempo de ejecución, la expresión se prueba en T. Si se produce un error, se produce un error en la
coincidencia del patrón de propiedad y el resultado es false . Si se realiza correctamente, cada
property_subpattern campo o propiedad se lee y su valor coincide con su patrón correspondiente. El resultado
de la coincidencia completa false solo es si el resultado de cualquiera de ellos es false . No se especifica el
orden en el que coinciden los subpatrones y es posible que no coincidan todos los subpatróns en tiempo de
ejecución. Si la coincidencia se realiza correctamente y el simple_designation del property_pattern es un
single_variable_designation, define una variable de tipo T que tiene asignado el valor coincidente.
Nota: el patrón de propiedad se puede usar para la coincidencia de patrones con tipos anónimos.
Ej e m p l o
if (o is string { Length: 5 } s)
Cambiar expresión
Se agrega una switch_expression a la semántica de compatibilidad con switch un contexto de expresión.
La sintaxis del lenguaje C# se amplía con las siguientes producciones sintácticas:
multiplicative_expression
: switch_expression
| multiplicative_expression '*' switch_expression
| multiplicative_expression '/' switch_expression
| multiplicative_expression '%' switch_expression
;
switch_expression
: range_expression 'switch' '{' '}'
| range_expression 'switch' '{' switch_expression_arms ','? '}'
;
switch_expression_arms
: switch_expression_arm
| switch_expression_arms ',' switch_expression_arm
;
switch_expression_arm
: pattern case_guard? '=>' expression
;
case_guard
: 'when' null_coalescing_expression
;
El tipo del switch_expression es el mejor tipo común de las expresiones que aparecen a la derecha de los =>
tokens de la switch_expression_arm s si este tipo existe y la expresión en cada brazo de la expresión switch se
puede convertir implícitamente a ese tipo. Además, se agrega una nueva conversión de expresión switch, que es
una conversión implícita predefinida de una expresión switch a cada tipo T para el que existe una conversión
implícita de cada expresión de ARM en T .
Es un error si algún patrón de switch_expression_arm no puede afectar al resultado porque algún patrón y una
protección anteriores siempre coincidirán.
Se dice que una expresión switch es exhaustiva si algún brazo de la expresión switch controla cada valor de su
entrada. El compilador generará una advertencia si una expresión switch no es exhaustiva.
En tiempo de ejecución, el resultado de la switch_expression es el valor de la expresión de la primera
switch_expression_arm para la que la expresión del lado izquierdo del switch_expression coincide con el patrón
del switch_expression_arm y para el que el case_guard de la switch_expression_arm, si está presente, se evalúa
como true . Si no existe tal switch_expression_arm, el switch_expression inicia una instancia de la excepción
System.Runtime.CompilerServices.SwitchExpressionException .
Para permitir
switch (a, b)
{
los paréntesis de la instrucción switch son opcionales cuando la expresión que se está cambiando es un literal
de tupla.
Orden de evaluación en coincidencia de patrones
Ofrecer la flexibilidad del compilador al reordenar las operaciones ejecutadas durante la coincidencia de
patrones puede permitir la flexibilidad que se puede utilizar para mejorar la eficacia de la coincidencia de
patrones. El requisito (no exigido) sería que las propiedades a las que se tiene acceso en un patrón y los
métodos de deconstrucción deben ser "puras" (sin efecto secundario, idempotente, etc.). Eso no significa que se
agregue la pureza como concepto de lenguaje, solo que se permitiría la flexibilidad del compilador en las
operaciones de reordenación.
Resolución 2018-04-04 LDM : confirmada: el compilador puede reordenar las llamadas a Deconstruct , los
accesos a las propiedades y las invocaciones de los métodos en ITuple , y puede suponer que los valores
devueltos son los mismos desde varias llamadas. El compilador no debe invocar las funciones que no pueden
afectar al resultado, por lo que tendremos mucho cuidado antes de realizar cambios en el orden generado por el
compilador de la evaluación en el futuro.
Algunas optimizaciones posibles
La compilación de la coincidencia de patrones puede aprovechar las partes comunes de los patrones. Por
ejemplo, si la prueba de tipo de nivel superior de dos patrones sucesivos de un switch_statement es del mismo
tipo, el código generado puede omitir la prueba de tipo para el segundo patrón.
Cuando algunos de los patrones son enteros o cadenas, el compilador puede generar el mismo tipo de código
que genera para una instrucción switch en versiones anteriores del lenguaje.
Para obtener más información sobre estos tipos de optimizaciones, vea [Scott and Ramsey (2000)].
métodos de interfaz predeterminados
18/09/2021 • 52 minutes to read
[x] propuesto
[] Prototipo: en curso
[] Implementación: ninguno
[] Especificación: en curso, a continuación
Resumen
Agregar compatibilidad con métodos de extensión virtual : métodos en interfaces con implementaciones
concretas. Una clase o estructura que implementa este tipo de interfaz debe tener una única implementación
más específica para el método de interfaz, implementada por la clase o struct, o heredada de sus clases base o
interfaces. Los métodos de extensión virtual permiten a un autor de la API agregar métodos a una interfaz en
versiones futuras sin interrumpir la compatibilidad binaria o de origen con las implementaciones existentes de
esa interfaz.
Son similares a los "métodos predeterminados"de Java.
(Según la técnica de implementación probable) esta característica requiere la compatibilidad correspondiente en
la CLI o CLR. Los programas que aprovechan esta característica no se pueden ejecutar en versiones anteriores
de la plataforma.
Motivación
Las motivaciones principales de esta característica son
Los métodos de interfaz predeterminados permiten a un autor de la API agregar métodos a una interfaz en
versiones futuras sin interrumpir la compatibilidad binaria o de origen con las implementaciones existentes
de esa interfaz.
La característica permite que C# interopere con las API que tienen como destino Android (Java) e iOS
(SWIFT), que admiten características similares.
A medida que se pasa, agregar implementaciones de interfaces predeterminadas proporciona los elementos
de la característica de lenguaje "rasgos" ( https://en.wikipedia.org/wiki/Trait_(computer_programming) ). Los
rasgos han demostrado ser una técnica de programación eficaz (
http://scg.unibe.ch/archive/papers/Scha03aTraits.pdf ).
Diseño detallado
La sintaxis de una interfaz se extiende para permitir
declaraciones de miembros que declaran constantes, operadores, constructores estáticos y tipos anidados;
cuerpo para un método o indizador, propiedad o descriptor de acceso de eventos (es decir, una
implementación "predeterminada");
declaraciones de miembros que declaran campos estáticos, métodos, propiedades, indizadores y eventos;
declaraciones de miembros mediante la sintaxis de implementación de interfaz explícita; etc
Modificadores de acceso explícitos (el acceso predeterminado es public ).
Los miembros con cuerpos permiten que la interfaz proporcione una implementación "predeterminada" para el
método en clases y Structs que no proporcionan una implementación de reemplazo.
Es posible que las interfaces no contengan estado de instancia. Aunque los campos estáticos ahora están
permitidos, los campos de instancia no se permiten en las interfaces. Las propiedades automáticas de instancia
no se admiten en las interfaces, ya que declararían de forma implícita un campo oculto.
Los métodos estáticos y privados permiten una refactorización y organización de código útiles que se usan para
implementar la API pública de la interfaz.
Una invalidación de método en una interfaz debe utilizar la sintaxis de implementación de interfaz explícita.
Es un error declarar un tipo de clase, un tipo de estructura o un tipo de enumeración dentro del ámbito de un
parámetro de tipo declarado con un variance_annotation. Por ejemplo, la declaración C siguiente es un error.
interface IA
{
void M() { WriteLine("IA.M"); }
}
Una clase que implementa esta interfaz no necesita implementar su método concreto.
class C : IA { } // OK
IA i = new C();
i.M(); // prints "IA.M"
La última invalidación de IA.M en C la clase es el método concreto M declarado en IA . Tenga en cuenta que
una clase no hereda miembros de sus interfaces; Esto no se modifica en esta característica:
new C().M(); // error: class 'C' does not contain a member 'M'
Dentro de un miembro de instancia de una interfaz, this tiene el tipo de la interfaz envolvente.
Modificadores en interfaces
La sintaxis de una interfaz es relajada para permitir modificadores en sus miembros. Se permiten los siguientes
elementos: private , protected , internal , public , virtual , abstract ,,, sealed static extern y
partial .
Un miembro de interfaz cuya declaración incluye un cuerpo es un virtual miembro a menos que sealed
private se use el modificador o. El virtual modificador se puede usar en un miembro de función que, de otro
modo, sería implícitamente virtual . Del mismo modo, aunque abstract es el valor predeterminado en los
miembros de interfaz sin cuerpos, ese modificador se puede proporcionar explícitamente. Un miembro no
virtual se puede declarar con la sealed palabra clave.
Es un error que un private sealed miembro de función o de una interfaz no tenga cuerpo. Un private
miembro de función no puede tener el modificador sealed .
Los modificadores de acceso se pueden usar en los miembros de interfaz de todos los tipos de miembros que se
permiten. El nivel de acceso public es el valor predeterminado, pero se puede proporcionar explícitamente.
Problema abier to: Es necesario especificar el significado preciso de los modificadores de acceso, como
protected y internal , y qué declaraciones hacen y no invalidan (en una interfaz derivada) ni se
implementan (en una clase que implementa la interfaz).
Las interfaces pueden declarar static miembros, incluidos los tipos anidados, los métodos, los indexadores, las
propiedades, los eventos y los constructores estáticos. El nivel de acceso predeterminado para todos los
miembros de interfaz es public .
Es posible que las interfaces no declaren constructores de instancias, destructores o campos.
*Problema cerrado: _ ¿se permiten las declaraciones de operador en una interfaz? Probablemente no se
trate de operadores de conversión, pero ¿qué ocurre con otros? Decisión: se permiten los operadores except
* para los operadores de conversión, igualdad y desigualdad.
*Problema cerrado: _ new se debe permitir en las declaraciones de miembros de interfaz que ocultan a
los miembros de las interfaces base. _ Decisión *: sí.
*Problema cerrado: _ actualmente no se permite partial en una interfaz ni en sus miembros. Eso
requeriría una propuesta independiente. _ Decisión *: sí.
https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#permit-partial-
in-interface
Invalidaciones en interfaces
Las declaraciones de invalidación (es decir, las que contienen el override modificador) permiten al
programador proporcionar una implementación más específica de un miembro virtual en una interfaz en la que
el compilador o el tiempo de ejecución no encontrarían ninguno. También permite convertir un miembro
abstracto de una superinterfaz en un miembro predeterminado en una interfaz derivada. Se permite que una
declaración de invalidación invalide explícitamente un método de interfaz base determinado calificando la
declaración con el nombre de la interfaz (en este caso no se permite el modificador de acceso). No se permiten
las invalidaciones implícitas.
interface IA
{
void M() { WriteLine("IA.M"); }
}
interface IB : IA
{
override void IA.M() { WriteLine("IB.M"); } // explicitly named
}
interface IC : IA
{
override void M() { WriteLine("IC.M"); } // implicitly named
}
interface IA
{
void M() { WriteLine("IA.M"); }
}
interface IB : IA
{
abstract void IA.M();
}
class C : IB { } // error: class 'C' does not implement 'IA.M'.
Por ejemplo:
interface IA
{
void M() { WriteLine("IA.M"); }
}
interface IB : IA
{
void IA.M() { WriteLine("IB.M"); }
}
interface IC : IA
{
void IA.M() { WriteLine("IC.M"); }
}
interface ID : IB, IC { } // error: no most specific override for 'IA.M'
abstract class C : IB, IC { } // error: no most specific override for 'IA.M'
abstract class D : IA, IB, IC // ok
{
public abstract void M();
}
La regla de invalidación más específica garantiza que el programador resuelva explícitamente un conflicto (es
decir, una ambigüedad derivada de la herencia de rombo) en el punto en que se produce el conflicto.
Dado que se admiten invalidaciones abstractas explícitas en interfaces, podríamos hacerlo también en clases.
Problema de aper tura : ¿se admiten invalidaciones abstractas de interfaz explícitas en las clases?
Además, es un error si en una declaración de clase la invalidación más específica de algún método de interfaz es
una invalidación abstracta que se declaró en una interfaz. Se trata de una regla existente que se ha reestado con
la nueva terminología.
interface IF
{
void M();
}
abstract class F : IF { } // error: 'F' does not implement 'IF.M'
Es posible que una propiedad virtual declarada en una interfaz tenga una invalidación más específica para su
get descriptor de acceso en una interfaz y una invalidación más específica para su set descriptor de acceso
en una interfaz diferente. Esto se considera una infracción de la regla de invalidación más específica .
Métodos static y private
Dado que las interfaces pueden contener ahora código ejecutable, resulta útil abstraer el código común en
métodos privados y estáticos. Ahora se permiten en interfaces.
*Problema cerrado _: ¿se deben admitir métodos privados? ¿Se debe admitir métodos estáticos?
• Decisión: sí*
Problema de aper tura : ¿se permite que los métodos de interfaz sean protected u internal u otro
acceso? Si es así, ¿cuál es la semántica? ¿Son virtual de forma predeterminada? Si es así, ¿hay alguna
manera de hacer que sean no virtuales?
Problema abier to : si se admiten métodos estáticos, ¿se deben admitir operadores estáticos?
interface I0
{
void M() { Console.WriteLine("I0"); }
}
interface I1 : I0
{
override void M() { Console.WriteLine("I1"); }
}
interface I2 : I0
{
override void M() { Console.WriteLine("I2"); }
}
interface I3 : I1, I2
{
// an explicit override that invoke's a base interface's default method
void I0.M() { I2.base.M(); }
}
interface IA
{
void M() { WriteLine("IA.M"); }
}
interface IB : IA
{
override void IA.M() { WriteLine("IB.M"); }
}
interface IC : IA
{
override void IA.M() { WriteLine("IC.M"); }
}
Cuando virtual abstract se tiene acceso a un miembro o mediante la sintaxis base(Type).M , es necesario
que Type contenga una invalidación exclusiva más específica para M .
Enlazar cláusulas base
Las interfaces ahora contienen tipos. Estos tipos se pueden usar en la cláusula base como interfaces base. Al
enlazar una cláusula base, es posible que sea necesario conocer el conjunto de interfaces base para enlazar esos
tipos (por ejemplo, para buscar en ellos y para resolver el acceso protegido). Por lo tanto, el significado de la
cláusula base de una interfaz se define circularmente. Para interrumpir el ciclo, se agregan nuevas reglas de
lenguaje correspondientes a una regla similar ya implementada para las clases.
A la hora de determinar el significado del interface_base de una interfaz, se supone que las interfaces base están
vacías de forma temporal. Intuitivamente esto garantiza que el significado de una cláusula base no puede
depender recursivamente.
Hemos usado las siguientes reglas:
"Cuando una clase B se deriva de una clase a, es un error en tiempo de compilación para que una dependa de B.
Una clase depende directamente de su clase base directa (si existe) y depende directamente de la clase
en la que se anida inmediatamente (si existe). Dada esta definición, el conjunto completo de clases de las que
depende una clase es el cierre reflexivo y transitivo de la relación directamente depende de . "
Se trata de un error en tiempo de compilación para una interfaz que hereda directa o indirectamente de sí
misma. Las interfaces base de una interfaz son las interfaces base explícitas y sus interfaces base. En otras
palabras, el conjunto de interfaces base es el cierre transitivo completo de las interfaces base explícitas, sus
interfaces base explícitas, etc.
Vamos a ajustarlos de la siguiente manera:
Cuando una clase B se deriva de una clase a, se trata de un error en tiempo de compilación para que dependa de
B. Una clase depende directamente de su clase base directa (si existe) y depende directamente del tipo en
el que se anida inmediatamente (si existe).
Cuando una interfaz IB extiende una interfaz IA, se trata de un error en tiempo de compilación para que IA
dependa de IB. Una interfaz depende directamente de sus interfaces base directas (si hay alguna) y depende
directamente del tipo en el que se anida inmediatamente (si existe).
Dadas estas definiciones, el conjunto completo de tipos de los que depende un tipo es el cierre reflexivo y
transitivo de la relación directamente depende de .
Efecto en los programas existentes
Las reglas que se presentan aquí están destinadas a no tener ningún efecto en el significado de los programas
existentes.
Ejemplo 1:
interface IA
{
void M();
}
class C: IA // Error: IA.M has no concrete most specific override in C
{
public static void M() { } // method unrelated to 'IA.M' because static
}
Ejemplo 2:
interface IA
{
void M();
}
class Base: IA
{
void IA.M() { }
}
class Derived: Base, IA // OK, all interface members have a concrete most specific override
{
private void M() { } // method unrelated to 'IA.M' because private
}
Las mismas reglas proporcionan resultados similares a la situación análoga en la que intervienen los métodos
de interfaz predeterminados:
interface IA
{
void M() { }
}
class Derived: IA // OK, all interface members have a concrete most specific override
{
private void M() { } // method unrelated to 'IA.M' because private
}
*Problema cerrado _: confirme que se trata de una consecuencia prevista de la especificación. • Decisión:
sí*
namespace System.Runtime.CompilerServices
{
public static class RuntimeFeature
{
// Presence of the field indicates runtime support
public const string DefaultInterfaceImplementation = nameof(DefaultInterfaceImplementation);
}
}
*Open Issue _: ¿Cuál es el mejor nombre para la característica _CLR *? La característica CLR hace mucho
más que eso (por ejemplo, relaja las restricciones de protección, admite invalidaciones en interfaces, etc.).
Quizás se deba llamar a algo como "métodos concretos en interfaces" o "rasgos"?
Inconvenientes
Esta propuesta requiere una actualización coordinada de la especificación CLR (para admitir métodos concretos
en interfaces y resolución de métodos). Por lo tanto, es bastante "costosa" y puede que merezca la pena hacerlo
en combinación con otras características que también se prevén para requerir cambios en CLR.
Alternativas
Ninguno.
Preguntas no resueltas
Las preguntas abiertas se mencionan a lo largo de la propuesta anterior.
Vea también https://github.com/dotnet/csharplang/issues/406 para obtener una lista de preguntas abiertas.
La especificación detallada debe describir el mecanismo de resolución que se utiliza en tiempo de ejecución
para seleccionar el método preciso que se va a invocar.
La interacción de los metadatos generados por los nuevos compiladores y consumidos por los compiladores
más antiguos debe trabajar en detalle. Por ejemplo, debemos asegurarnos de que la representación de los
metadatos que usamos no provoca la adición de una implementación predeterminada en una interfaz para
interrumpir una clase existente que implementa esa interfaz cuando la compila un compilador anterior. Esto
puede afectar a la representación de los metadatos que se puede usar.
El diseño debe tener en cuenta la interoperación con otros lenguajes y los compiladores existentes para otros
lenguajes.
Preguntas resueltas
Invalidación abstracta
La antigua especificación de borrador contenía la capacidad de "reabstraer" un método heredado:
interface IA
{
void M();
}
interface IB : IA
{
override void M() { }
}
interface IC : IB
{
override void M(); // make it abstract again
}
Mis notas de 2017-03-20 mostraron que decidimos no permitirlo. Sin embargo, hay al menos dos casos de uso
para ella:
1. Las API de Java, con las que algunos usuarios de esta característica esperan interoperar, dependen de esta
instalación.
2. La programación con rasgos se beneficia de este. La reabstracción es uno de los elementos de la
característica de lenguaje "rasgos" ( https://en.wikipedia.org/wiki/Trait_(computer_programming)) . Se
permite lo siguiente con las clases:
Desafortunadamente, este código no se puede refactorizar como un conjunto de interfaces (rasgos) a menos
que se permita. Por el principio de Jared de expansivo, se debe permitir.
Problema cerrado: ¿Se debe permitir la reabstracción? ? Mis notas eran incorrectas. Las notas LDM
indican que se permite la reabstracción en una interfaz. No está en una clase.
Decidimos permitir los modificadores indicados explícitamente en los miembros de la interfaz, a menos que
haya una razón para no permitir algunos de ellos. Esto ofrece una pregunta interesante sobre el modificador
virtual. ¿Debe ser necesario en los miembros con implementación predeterminada?
Podríamos decir que:
Si no hay ninguna implementación y no se especifican ni virtual ni Sealed, se supone que el miembro es
abstracto.
Si hay una implementación de y no se especifican Abstract, ni Sealed, se supone que el miembro es
virtual.
el modificador Sealed es necesario para que un método no sea virtual ni abstracto.
Como alternativa, podríamos indicar que el modificador virtual es necesario para un miembro virtual. Es
decir, si hay un miembro con una implementación no marcada explícitamente con el modificador virtual, no
es virtual ni abstracta. Este enfoque puede proporcionar una mejor experiencia cuando se mueve un método
de una clase a una interfaz:
un método abstracto permanece abstracto.
un método virtual sigue siendo virtual.
un método sin modificador no sigue siendo virtual ni abstracto.
el modificador Sealed no se puede aplicar a un método que no sea una invalidación.
¿Qué opina?
Problema cerrado: ¿Debe un método concreto (con implementación) ser implícitamente virtual ??
Sabemos que la implementación de I1.M en C es I1.M . Qué sucede si el ensamblado que contiene I2 se
cambia como se indica a continuación y se vuelve a compilar
interface I2 : I1
{
override void M() { Impl2 }
}
pero C no se vuelve a compilar. ¿Qué ocurre cuando se ejecuta el programa? Una invocación de (C as I1).M()
1. Saturación I1.M
2. Saturación I2.M
3. Produce algún tipo de error en tiempo de ejecución
Decisión: Realizado 2017-04-11: ejecuta I2.M , que es la invalidación más específica de forma inequívoca en
tiempo de ejecución.
Descriptores de acceso de eventos (cerrados)
Problema cerrado: ¿Se puede invalidar un evento "a trozos"?
public interface I1
{
event T e1;
}
public interface I2 : I1
{
override event T
{
add { }
// error: "remove" accessor missing
}
}
Esta implementación "parcial" del evento no está permitida porque, como en una clase, la sintaxis de una
declaración de evento no permite solo un descriptor de acceso; deben proporcionarse ambos (o ninguno).
Puede lograr lo mismo si se permite que el descriptor de acceso Remove abstracto de la sintaxis sea abstracto
implícitamente por la ausencia de un cuerpo:
public interface I1
{
event T e1;
}
public interface I2 : I1
{
override event T
{
add { }
remove; // implicitly abstract
}
}
Tenga en cuenta que se trata de una nueva sintaxis (propuesta). En la gramática actual, los descriptores de
acceso de eventos tienen un cuerpo obligatorio.
Problema cerrado: Puede que un descriptor de acceso de eventos sea abstracto (implícitamente) por la
omisión de un cuerpo, de forma similar a la forma en que los métodos de las interfaces y los descriptores de
acceso de propiedad son abstractos por la omisión de un cuerpo.
Decisión: (2017-04-18) no, las declaraciones de eventos requieren ambos descriptores de acceso concretos (o
ninguno).
Reabstracción en una clase (cerrada)
Problema cerrado: Debemos confirmar que esto se permite (de lo contrario, agregar una implementación
predeterminada sería un cambio importante):
interface I1
{
void M() { }
}
abstract class C : I1
{
public abstract void M(); // implement I1.M with an abstract method in C
}
Decisión: (2017-04-18) sí, agregar un cuerpo a una declaración de miembro de interfaz no debe romper C.
Invalidación sellada (cerrada)
La pregunta anterior presupone implícitamente que el sealed modificador se puede aplicar a un override en
una interfaz. Esto contradice la especificación del borrador. ¿Queremos permitir el sellado de una invalidación?
Se deben tener en cuenta los efectos de la compatibilidad binaria y de origen del sellado.
Decisión: (2017-04-18) no se permite sealed en las invalidaciones de las interfaces. El único uso de sealed
en los miembros de interfaz es hacer que no sean virtuales en su declaración inicial.
Herencia y clases de Diamond (cerradas)
El borrador de la propuesta prefiere invalidaciones de clase a invalidaciones de interfaz en escenarios de
herencia de Diamond:
Es necesario que todas las interfaces y clases tengan una invalidación más específica para cada método de
interfaz entre las invalidaciones que aparecen en el tipo o sus interfaces directas e indirectas. La invalidación
más específica es una invalidación única que es más específica que cualquier otra invalidación. Si no hay
ninguna invalidación, el propio método se considera la invalidación más específica.
Una invalidación M1 se considera más específica que otra invalidación M2 si M1 se declara en el tipo T1 ,
M2 se declara en el tipo T2 y
El escenario es este
interface IA
{
void M();
}
interface IB : IA
{
override void M() { WriteLine("IB"); }
}
class Base : IA
{
void IA.M() { WriteLine("Base"); }
}
class Derived : Base, IB // allowed?
{
static void Main()
{
Ia a = new Derived();
a.M(); // what does it do?
}
}
*Problema cerrado: _ confirmar la especificación del borrador, anteriormente, por _most reemplazo
específico * cuando se aplica a clases e interfaces mixtas (una clase tiene prioridad sobre una interfaz). Vea
https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-19.md#diamonds-with-
classes.
interface IA
{
public void M() { }
}
struct S : IA
{
}
var s = default(S);
s.M(); // error: 'S' does not contain a member 'M'
Por consiguiente, el cliente debe tener Box el struct para invocar los métodos de interfaz
IA s = default(S); // an S, boxed
s.M(); // ok
La conversión boxing de este modo anula las ventajas principales de un struct tipo. Además, los métodos de
mutación no tendrán ningún efecto aparente, ya que funcionan en una copia con conversión boxing del struct:
interface IB
{
public void Increment() { P += 1; }
public int P { get; set; }
}
struct T : IB
{
public int P { get; set; } // auto-property
}
T t = default(T);
Console.WriteLine(t.P); // prints 0
(t as IB).Increment();
Console.WriteLine(t.P); // prints 0
Problema abier to: ¿Deben permitirse las invocaciones de interfaz base en los miembros de clase?
Problema cerrado: Si es una invalidación "implícita" que no denomina la interfaz, ¿debe coincidir el
modificador de acceso?
Decisión: Solo los miembros públicos se pueden invalidar implícitamente y el acceso debe coincidir. Vea
https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-18.md#dim-implementing-
a-non-public-interface-member-not-in-list.
Problema abier to: ¿El modificador de acceso es obligatorio, opcional u omitido en una invalidación
explícita como override void IB.M() {} ?
Problema abier to: override ¿Es obligatorio, opcional u omitido en una invalidación explícita, como
void IB.M() {} ?
¿Cómo implementa un miembro de interfaz no pública en una clase? ¿Es posible que se realice explícitamente?
interface IA
{
internal void MI();
protected void MP();
}
class C : IA
{
// are these implementations?
internal void MI() {}
protected void MP() {}
}
Sabemos que la implementación de I1.M en C es I2.M . Qué sucede si el ensamblado que contiene I3 se
cambia como se indica a continuación y se vuelve a compilar
interface I3 : I1
{
override void M() { Impl3 }
}
pero C no se vuelve a compilar. ¿Qué ocurre cuando se ejecuta el programa? Una invocación de (C as I1).M()
1. Saturación I1.M
2. Saturación I2.M
3. Saturación I3.M
4. 2 o 3, de manera determinista
5. Produce algún tipo de excepción en tiempo de ejecución
Decisión : inicia una excepción (5). Vea
https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#issues-in-default-
interface-methods.
partial ¿Permitir en la interfaz? subtítulos
Dado que las interfaces se pueden usar de maneras análogas a la forma en que se usan las clases abstractas,
puede ser útil declararlas partial . Esto sería especialmente útil en el caso de los generadores.
Propuesta: Quite la restricción de lenguaje que las interfaces y los miembros de las interfaces no se pueden
declarar partial .
Problema abier to: ¿Es un static Main método de una interfaz un candidato para ser el punto de entrada
del programa?
Problema semicerrado: (2017-04-18) creemos que será útil, pero volveremos a él. Se trata de un bloque
de recorrido de modelos mental.
interface IA
{
void M(int x) { }
}
interface IB : IA
{
override void M(int y) { }
}
interface IC : IB
{
static void M2()
{
M(y: 3); // permitted?
}
override void IB.M(int z) { } // permitted? What does it override?
}
Problema abier to: ¿Una declaración de invalidación en una interfaz introduce un nuevo miembro?
subtítulos
En una clase, un método de invalidación es visible en algunos sentidos. Por ejemplo, los nombres de sus
parámetros tienen prioridad sobre los nombres de los parámetros del método invalidado. Es posible que se
duplique ese comportamiento en las interfaces, ya que siempre hay una invalidación más específica. ¿Pero
queremos duplicar ese comportamiento?
Además, es posible "invalidar" un método de invalidación? [Moot]
Decision : no se override permite ninguna palabra clave en los miembros de la interfaz.
https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#does-an-override-in-
an-interface-introduce-a-new-member.
Propiedades con un descriptor de acceso privado (cerrado )
Suponemos que los miembros privados no son virtuales y la combinación de virtual y privada no está
permitida. Pero ¿qué ocurre con una propiedad con un descriptor de acceso privado?
interface IA
{
public virtual int P
{
get => 3;
private set => { }
}
}
¿Se permite? ¿Es el set descriptor de acceso aquí virtual o no? ¿Se puede invalidar Cuando sea accesible?
¿Lo siguiente implementa implícitamente solo el get descriptor de acceso?
class C : IA
{
public int P
{
get => 4;
set { }
}
}
Es, supuestamente, un error debido a que IA. P. set no es virtual y también porque no se puede acceder a él.
class C : IA
{
int IA.P
{
get => 4;
set { }
}
}
Decisión : el primer ejemplo es válido, mientras que el último no lo es. Esto se resuelve de forma análoga a
como funciona en C#. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-
17.md#properties-with-a-private-accessor
Invocaciones de interfaz base, Round 2 (cerrado )
Nuestra "solución" anterior a la administración de las invocaciones base no proporciona una expresividad
suficiente. Resulta que en C# y en CLR, a diferencia de Java, debe especificar tanto la interfaz que contiene la
declaración de método como la ubicación de la implementación que desea invocar.
Propongo la siguiente sintaxis para llamadas base en interfaces. No me encanta con él, pero muestra lo que
cualquier sintaxis debe poder expresar:
interface I1 { void M(); }
interface I2 { void M(); }
interface I3 : I1, I2 { void I1.M() { } void I2.M() { } }
interface I4 : I1, I2 { void I1.M() { } void I2.M() { } }
interface I5 : I3, I4
{
void I1.M()
{
base<I3>(I1).M(); // calls I3's implementation of I1.M
base<I4>(I1).M(); // calls I4's implementation of I1.M
}
void I2.M()
{
base<I3>(I2).M(); // calls I3's implementation of I2.M
base<I4>(I2).M(); // calls I4's implementation of I2.M
}
}
Or
Or
Design Meetings
Notas de la reunión de LDM 2017-03-08 Notas de la reunión de LDM 2017-03-21 2017-03-23 Meeting
"comportamiento de CLR para los métodos de interfaz predeterminados" Notas de la reunión de LDM 2017-04-
05 Notas de la reunión de LDM 2017-04-11 Notas de la reunión de LDM 2017-04-18 Notas de la reunión de
LDM 2017-04-19 Notas de la reunión de LDM 2017-05-17 Notas de la reunión de LDM 2017-05-31 Notas de la
reunión de LDM 2017-06-14 Notas de la reunión de LDM 2018-10-17 notas de la reunión de LDM 2018-11-14
Secuencias asincrónicas
18/09/2021 • 40 minutes to read
[x] propuesto
[x] prototipo
[] Implementación
[] Especificación
Resumen
C# es compatible con métodos de iterador y métodos asincrónicos, pero no admite un método que sea un
iterador y un método asincrónico. Para solucionar este error, se permite que await se use en una nueva forma
de async iterador, una que devuelve un IAsyncEnumerable<T> o IAsyncEnumerator<T> en lugar de
IEnumerable<T> o IEnumerator<T> , con IAsyncEnumerable<T> consumible en un nuevo await foreach . Una
IAsyncDisposable interfaz también se usa para habilitar la limpieza asincrónica.
Discusión relacionada
https://github.com/dotnet/roslyn/issues/261
https://github.com/dotnet/roslyn/issues/114
Diseño detallado
Interfaces
IAsyncDisposable
Ha habido mucha información IAsyncDisposable (por ejemplo, https://github.com/dotnet/roslyn/issues/114) y
si es una buena idea). Sin embargo, es un concepto necesario para agregar compatibilidad con iteradores
asincrónicos. Dado que los finally bloques pueden contener await s y, dado que los finally bloques deben
ejecutarse como parte de la eliminación de iteradores, se necesita la eliminación asincrónica. También es muy útil
en general cuando la limpieza de recursos puede tardar cualquier período de tiempo, por ejemplo, cerrar los
archivos (que requieren vaciados), anular el registro de las devoluciones de llamada y proporcionar una manera
de saber cuándo se ha completado el registro, etc.
La siguiente interfaz se agrega a las bibliotecas básicas de .NET (por ejemplo, System. Private. CoreLib/System.
Runtime):
namespace System
{
public interface IAsyncDisposable
{
ValueTask DisposeAsync();
}
}
Al igual que con, la invocación de Dispose DisposeAsync varias veces es aceptable y las invocaciones
subsiguientes después de la primera deben tratarse como Nops, devolviendo una tarea completada
correctamente completa ( DisposeAsync no es necesario que sea segura para subprocesos, sin embargo, y no es
necesario que admitan la invocación simultánea). Además, los tipos pueden implementar IDisposable y
IAsyncDisposable , y si lo hacen, es de igual forma aceptable invocar Dispose y, a continuación, DisposeAsync o
viceversa, pero solo el primero debe ser significativo y las invocaciones subsiguientes de deben ser una
instrucción NOP. Como tal, si un tipo implementa ambos, se anima a los consumidores a llamar una vez y solo
una vez al método más relevante basado en el contexto, Dispose en los contextos sincrónicos y DisposeAsync
en los asincrónicos.
(Estoy saliendo de cómo IAsyncDisposable interactúa con using para obtener una explicación independiente).
La cobertura de cómo interactúa con foreach se trata más adelante en esta propuesta).
Alternativas consideradas:
DisposeAsync aceptar un CancellationToken : aunque en teoría tiene sentido que se puede cancelar cualquier
elemento asincrónico, la eliminación es acerca de la limpieza, el cierre de las cosas, los recursos de free'ing,
etc., que normalmente no es algo que se debe cancelar; la limpieza sigue siendo importante para el trabajo
que se cancela. Lo mismo CancellationToken que hizo que se cancelara el trabajo real sería el mismo token
que se pasa a DisposeAsync , lo que resultaría DisposeAsync útil porque la cancelación del trabajo podría
provocar DisposeAsync que se tratara de una instrucción NOP. Si alguien desea evitar que se bloquee a la
espera de la eliminación, puede evitar la espera en el resultado ValueTask o esperar solo durante un período
de tiempo.
DisposeAsync devolviendo Task un: ahora que existe un no genérico ValueTask y se puede construir a
partir de, si se IValueTaskSource devuelve ValueTask desde DisposeAsync permite que un objeto existente
se reutilice como el compromiso que representa la finalización asincrónica eventual de DisposeAsync ,
guardando una Task asignación en el caso de que se DisposeAsync complete de forma asincrónica.
Configurar DisposeAsync con bool continueOnCapturedContext ( ConfigureAwait ): aunque puede haber
problemas relacionados con el modo en que este concepto se expone a using , foreach , y otras
construcciones de lenguaje que consumen esto, desde una perspectiva de interfaz, no está realizando
realmente ninguna await "operación" y no hay nada que configurar... los consumidores de ValueTask
pueden utilizarlo sin embargo.
heredar: dado que solo se debe usar una u otra, no tiene sentido forzar tipos para implementar ambos.
IAsyncDisposable IDisposable
IDisposableAsync en lugar de IAsyncDisposable : hemos seguido el nombre de los elementos y tipos que son
"Async", mientras que las operaciones son "Done Async", por lo que los tipos tienen "Async" como prefijo y
los métodos tienen "Async" como sufijo.
IAsyncEnumerable / IAsyncEnumerator
Se agregan dos interfaces a las bibliotecas .NET básicas:
namespace System.Collections.Generic
{
public interface IAsyncEnumerable<out T>
{
IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default);
}
El consumo típico (sin características adicionales del lenguaje) sería similar al siguiente:
IAsyncEnumerator<T> enumerator = enumerable.GetAsyncEnumerator();
try
{
while (await enumerator.MoveNextAsync())
{
Use(enumerator.Current);
}
}
finally { await enumerator.DisposeAsync(); }
namespace System.Collections.Generic
{
public interface IAsyncEnumerable<out T>
{
IAsyncEnumerator<T> GetAsyncEnumerator();
}
TryGetNextse utiliza en un bucle interno para consumir elementos con una única llamada de interfaz, siempre y
cuando estén disponibles de forma sincrónica. Cuando el siguiente elemento no se puede recuperar
sincrónicamente, devuelve false y cada vez que devuelve false, un llamador debe invocar posteriormente
WaitForNextAsync para esperar a que el elemento siguiente esté disponible o para determinar que nunca habrá
otro elemento. El consumo típico (sin características adicionales del lenguaje) sería similar al siguiente:
Desde una perspectiva meramente teórica, (5) es la más sólida, en la que (a) MoveNextAsync aceptar un
CancellationToken permite el control más preciso sobre lo que se cancela, y (b) CancellationToken es
simplemente cualquier otro tipo que se pueda pasar como un argumento en iteradores, incrustado en tipos
arbitrarios, etc.
Sin embargo, hay varios problemas con este enfoque:
¿Cómo CancellationToken se pasa en GetAsyncEnumerator el cuerpo del iterador? Podríamos exponer una
nueva iterator palabra clave de la que podría dejar punto fuera de para obtener acceso a la
CancellationToken pasada a GetEnumerator , pero a) eso es una gran cantidad de maquinaria adicional, b) la
estamos convirtiéndo en un ciudadano de primera clase y c) el caso del 99% sería el mismo código que
llama a un iterador y llama a GetAsyncEnumerator él, en cuyo caso puede pasar CancellationToken como
argumento al método.
¿Cómo CancellationToken se pasa a MoveNextAsync entrar en el cuerpo del método? Esto es incluso peor,
como si fuera un iterator objeto local, su valor podría cambiar entre las esperas, lo que significa que
cualquier código que se haya registrado con el token tendría que anular el registro de este antes de la espera
y, a continuación, volver a registrarlo después; también puede resultar bastante caro tener que hacer esto
registrando y anulando el registro en cada MoveNextAsync llamada, independientemente de si la implementó
el compilador en un iterador o
¿Cómo cancela un desarrollador un foreach bucle? Si se realiza mediante la asignación de un
CancellationToken a un enumerador/enumerador, o bien a), es necesario admitir los foreach
enumeradores, que los eleva a ciudadanos de primera clase. y ahora debe empezar a pensar en un
ecosistema creado en torno a los enumeradores (por ejemplo, los métodos LINQ) o b) necesitamos insertar
el CancellationToken en el enumerable de todas formas si tenemos algún WithCancellation método de
extensión de IAsyncEnumerable<T> que almacenaría el token proporcionado y, a continuación, lo pasaría a la
estructura Enumerable ajustada GetAsyncEnumerator cuando GetAsyncEnumerator se invoque el en el struct
devuelto (omitiendo ese token O bien, puede usar simplemente el CancellationToken que tiene en el cuerpo
de la instrucción foreach.
Si se admiten las comprensión de las consultas, ¿cómo se CancellationToken GetEnumerator MoveNextAsync
puede pasar a cada cláusula? La manera más sencilla sería simplemente para que la cláusula la Capture,
momento en el que se GetAsyncEnumerator / pasa por alto el token que se pasa a MoveNextAsync .
Se recomienda una versión anterior de este documento (1), pero hemos cambiado a (4).
Los dos problemas principales de (1):
los productores de enumerables cancelables tienen que implementar algunos reutilizables y solo pueden
aprovechar la compatibilidad del compilador para que los iteradores asincrónicos implementen un
IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken) método.
es probable que muchos productores fueran tentados a agregar un CancellationToken parámetro a su firma
Async-Enumerable en su lugar, lo que impedirá que los consumidores pasen el token de cancelación que
deseen cuando se les proporcione un IAsyncEnumerable tipo.
foreach
foreach se aumentará para admitir además de IAsyncEnumerable<T> la compatibilidad existente con
IEnumerable<T> . Y será compatible con el equivalente de IAsyncEnumerable<T> como patrón si los miembros
pertinentes se exponen públicamente, recurriendo al uso de la interfaz directamente en caso contrario, para
habilitar las extensiones basadas en struct que eviten la asignación, así como el uso de awaitables alternativos
como el tipo de valor devuelto de MoveNextAsync y DisposeAsync .
Sintaxis
Uso de la sintaxis:
C# seguirá tratando enumerable como un Enumerable sincrónico, de modo que incluso si expone las API
pertinentes para los enumerables asincrónicos (exponiendo el patrón o implementando la interfaz), solo tendrá
en cuenta las API sincrónicas.
Para forzar foreach a, en su lugar, solo tenga en cuenta las API asincrónicas, await se inserta de la siguiente
manera:
No se proporcionará ninguna sintaxis que admita el uso de las API Async o Sync. el desarrollador debe elegir
según la sintaxis utilizada.
Opciones descartadas que se consideran:
foreach (var i in await enumerable) : Esta sintaxis ya es válida y cambiar su significado sería un cambio
importante. Esto significa que await enumerable , a continuación, se devuelve algo que se pueda iterar
sincrónicamente y, a continuación, recorrer en iteración sincrónicamente.
foreach (var i await in enumerable) , foreach (var await i in enumerable) ,
foreach (await var i in enumerable) : Estos todos sugieren que estamos esperando el siguiente elemento,
pero hay otras esperas implicadas en foreach. en concreto, si el Enumerable es IAsyncDisposable , se
determinará await su eliminación asincrónica. Ese Await es como el ámbito de foreach en lugar de para cada
elemento individual y, por lo tanto, la await palabra clave merece estar en el foreach nivel. Además, si se
asocia con el foreach nos nos ofrece una manera de describir el foreach con un término diferente, por
ejemplo, un "Await foreach". Pero, lo que es más importante, es el valor de tener en cuenta foreach la
sintaxis al mismo tiempo que la using sintaxis, de modo que permanezcan coherentes entre sí y
using (await ...) ya sea una sintaxis válida.
foreach await (var i in enumerable)
Este código:
Tenga en cuenta que este enfoque no permitirá ConfigureAwait usarse con enumerables basadas en patrones.
pero, de nuevo, ya es el caso de que ConfigureAwait solo se exponga como una extensión en Task / Task<T> /
ValueTask / ValueTask<T> y no se pueda aplicar a cosas que se pueden esperar arbitrarias, ya que solo tiene
sentido cuando se aplican a las tareas (controla un comportamiento implementado en la compatibilidad de
continuación de la tarea) y, por lo tanto, no tiene sentido cuando se usa un patrón en el que las cosas que se
Cualquier persona que devuelva cosas que puedan esperar puede proporcionar su propio comportamiento
personalizado en escenarios avanzados.
(Si podemos encontrar alguna manera de admitir una solución de nivel de ensamblado o ámbito
ConfigureAwait , esto no será necesario).
Iteradores Async
El lenguaje/compilador admitirá la generación IAsyncEnumerable<T> de s y s además de IAsyncEnumerator<T>
consumirlos. En la actualidad, el lenguaje admite la escritura de un iterador como:
static IEnumerable<int> MyIterator()
{
try
{
for (int i = 0; i < 100; i++)
{
Thread.Sleep(1000);
yield return i;
}
}
finally
{
Thread.Sleep(200);
Console.WriteLine("finally");
}
}
pero await no se puede usar en el cuerpo de estos iteradores. Agregaremos ese soporte.
Sintaxis
La compatibilidad de lenguajes existente para los iteradores deduce la naturaleza de iterador del método en
función de si contiene cualquier yield s. Lo mismo se aplicará a los iteradores asincrónicos. Estos iteradores
asincrónicos se delimitarán y se diferenciarán de los iteradores sincrónicos mediante async la adición a la firma
y, a continuación, deberán tener también IAsyncEnumerable<T> o IAsyncEnumerator<T> como su tipo de valor
devuelto. Por ejemplo, el ejemplo anterior podría escribirse como un iterador Async de la siguiente manera:
Alternativas consideradas:
No usar async en la firma: async es probable que el compilador de use sea técnicamente necesario, ya que
lo utiliza para determinar si await es válido en ese contexto. Pero incluso si no es necesario, hemos
establecido que await solo se puede usar en métodos marcados como async y parece importante
mantener la coherencia.
Habilitar generadores personalizados para IAsyncEnumerable<T> : es algo que podríamos echar en el futuro,
pero la maquinaria es complicada y no es compatible con los homólogos sincrónicos.
Tener una iterator palabra clave en la firma: Los iteradores asincrónicos usarían async iterator en la
firma y yield solo se podrían usar en async métodos que incluían iterator ; iterator a continuación,
serían opcionales en iteradores sincrónicos. En función de su perspectiva, esto tiene la ventaja de que resulta
muy evidente por la firma del método, si yield se permite y si el método está pensado realmente para
devolver instancias de tipo IAsyncEnumerable<T> en lugar de la fabricación de un compilador en función de si
el código usa yield o no. Pero es diferente de los iteradores sincrónicos, que no y no se pueden establecer
para que requieran uno. Además, algunos desarrolladores no le gustan la sintaxis adicional. Si estamos
diseñando desde cero, probablemente lo haría necesario, pero en este momento hay mucho más valor en
mantener los iteradores asincrónicos cerca de los iteradores de sincronización.
LINQ
Hay más de ~ 200 sobrecargas de métodos en la System.Linq.Enumerable clase, todas las cuales funcionan en
términos de IEnumerable<T> ; algunas de estas aceptan IEnumerable<T> , algunas de ellas producen
IEnumerable<T> y muchas. Agregar compatibilidad con LINQ para IAsyncEnumerable<T> podría suponer la
duplicación de todas estas sobrecargas para el mismo, por otro ~ 200. Y como IAsyncEnumerator<T> es probable
que sea más común como una entidad independiente en el mundo asincrónico que IEnumerator<T> en el
mundo sincrónico, podríamos necesitar otras sobrecargas de ~ 200 que funcionen con IAsyncEnumerator<T> .
Además, un gran número de sobrecargas se ocupan de los predicados (por ejemplo, Where que toma
Func<T, bool> ) y puede ser conveniente tener IAsyncEnumerable<T> sobrecargas basadas en que se traten de
predicados sincrónicos y asincrónicos (por ejemplo, Func<T, ValueTask<bool>> además de Func<T, bool> ).
Aunque esto no es aplicable a todas las sobrecargas ahora ~ 400 nuevas, un cálculo aproximado es que sería
aplicable a la mitad, lo que significa que hay otras sobrecargas de ~ 200, para un total de ~ 600 nuevos
métodos.
Es un número escalonado de API, con la posibilidad de incluso más cuando se tienen en cuenta las bibliotecas de
extensiones como las extensiones interactivas (IX). Pero IX ya tiene una implementación de muchos de ellos y no
parece ser una buena razón para duplicar el trabajo. en su lugar, deberíamos ayudar a la comunidad a mejorar
IX y recomendarlo para cuando los desarrolladores quieran usar LINQ con IAsyncEnumerable<T> .
También hay el problema de la sintaxis de la comprensión de las consultas. La naturaleza basada en patrones de
las comprensión de las consultas permitiría "simplemente trabajar" con algunos operadores, por ejemplo, si IX
proporciona los métodos siguientes:
Sin embargo, no hay ninguna sintaxis de comprensión de consulta que admita await el uso de en las cláusulas,
por lo que, si se agrega IX, por ejemplo:
pero no habría ninguna manera de escribirlo con la await línea en la select cláusula. Como un esfuerzo
independiente, podríamos considerar la adición de async { ... } expresiones al lenguaje, en cuyo momento
podríamos permitirles usarlas en las comprensión de las consultas y en su lugar se podía escribir lo siguiente:
o para permitir await que se use directamente en expresiones, como, por ejemplo, admitir async from . Sin
embargo, no es probable que un diseño aquí afecte al resto del conjunto de características o a la otra, y esto no
es una cuestión especialmente importante para invertir en este momento, por lo que la propuesta consiste en
no hacer nada más en este momento.
Resumen
Esta característica trata sobre la entrega de dos nuevos operadores que permiten la construcción de objetos y , y
su uso para indexar o segmentar System.Index System.Range colecciones en tiempo de ejecución.
Información general
Tipos y miembros conocidos
Para usar los nuevos formularios sintácticos para y , pueden ser necesarios nuevos tipos y miembros conocidos,
en función de qué formularios System.Index System.Range sintácticos se usen.
Para usar el operador "hat" ( ^ ), se requiere lo siguiente
namespace System
{
public readonly struct Index
{
public Index(int value, bool fromEnd);
}
}
Para usar el System.Index tipo como argumento en un acceso de elemento de matriz, se requiere el siguiente
miembro:
La .. System.Range sintaxis de requerirá el System.Range tipo , así como uno o varios de los miembros
siguientes:
namespace System
{
public readonly struct Range
{
public Range(System.Index start, System.Index end);
public static Range StartAt(System.Index start);
public static Range EndAt(System.Index end);
public static Range All { get; }
}
}
La .. sintaxis permite que no falte ninguno de sus argumentos. Independientemente del número de
argumentos, el Range constructor siempre es suficiente para usar la Range sintaxis . Sin embargo, si alguno de
los demás miembros está presente y falta uno o varios de los argumentos, se puede sustituir .. el miembro
adecuado.
Por último, para que un valor de tipo se utilice en una expresión de acceso de elemento de matriz, debe estar
presente System.Range el siguiente miembro:
namespace System.Runtime.CompilerServices
{
public static class RuntimeHelpers
{
public static T[] GetSubArray<T>(T[] array, System.Range range);
}
}
System.Index
C# no tiene ninguna manera de indexar una colección desde el final, sino que la mayoría de los indexadores
usan la noción "desde el principio" o hacen una expresión "length - i". Presentamos una nueva expresión index
que significa "desde el final". La característica presentará un nuevo operador "hat" de prefijo unario. Su
operando único debe ser convertible a System.Int32 . Se reduce a la llamada al método System.Index de
generador adecuado.
Aumentamos la gramática para unary_expression con el siguiente formato de sintaxis adicional:
unary_expression
: '^' unary_expression
;
A esto se le llama el operador index from end. El índice predefinido de los operadores finales es el siguiente:
El comportamiento de este operador solo se define para valores de entrada mayores o iguales que cero.
Ejemplos:
System.Range
C# no tiene ninguna manera sintáctica de acceder a "intervalos" o "segmentos" de colecciones. Normalmente,
los usuarios se ven obligados a implementar estructuras complejas para filtrar o operar en segmentos de
memoria o recurrir a métodos LINQ como list.Skip(5).Take(2) . Con la adición de y otros tipos similares, es
más importante tener este tipo de operación admitida en un nivel más profundo en el lenguaje o tiempo de
ejecución, y tener la System.Span<T> interfaz unificada.
El lenguaje presentará un nuevo operador de intervalo x..y . Es un operador de infijo binario que acepta dos
expresiones. Cualquiera de los operandos se puede omitir (ejemplos a continuación) y deben poder convertirse
a System.Index . Se bajará a la llamada al método System.Range de generador adecuado.
Reemplazamos las reglas de gramática de C# multiplicative_expression por lo siguiente (con el fin de introducir
un nuevo nivel de prioridad):
range_expression
: unary_expression
| range_expression? '..' range_expression?
;
multiplicative_expression
: range_expression
| multiplicative_expression '*' range_expression
| multiplicative_expression '/' range_expression
| multiplicative_expression '%' range_expression
;
Todas las formas del operador de intervalo tienen la misma prioridad. Este nuevo grupo de precedencia es
menor que los operadores unarios y mayor que los operadores aritméticos multiplicativos.
Llamamos al .. operador el operador de intervalo. El operador de intervalo integrado se puede entender
aproximadamente para corresponder a la invocación de un operador integrado de esta forma:
Ejemplos:
Además, debe tener una conversión implícita de , para evitar la necesidad de sobrecargar la combinación de
enteros e índices sobre firmas System.Index System.Int32 multidimensionales.
Para estos tipos, el lenguaje actuará como si hubiera un miembro de indexador del formulario, donde es el tipo
de valor devuelto del indexador basado, incluidas las anotaciones T this[Index index] T de int ref estilo.
El nuevo miembro tendrá los mismos miembros get y set con accesibilidad que el int indexador.
El nuevo indexador se implementará convirtiendo el argumento de tipo en un y emitiendo una llamada Index
int al int indexador basado. Para fines de discusión, vamos a usar el ejemplo de receiver[expr] . La
conversión de expr a se producirá de la siguiente int manera:
Cuando el argumento tiene el formato ^expr2 y el tipo de es , se expr2 int traducirá a
receiver.Length - expr2 .
De lo contrario, se traducirá como expr.GetOffset(receiver.Length) .
Esto permite a los desarrolladores usar la Index característica en tipos existentes sin necesidad de modificación.
Por ejemplo:
// Gets translated to
var value = list[list.Count - 1];
Las expresiones y se desbordarán según corresponda para garantizar que los efectos secundarios solo
receiver Length se ejecuten una vez. Por ejemplo:
class Collection {
private int[] _array = new[] { 1, 2, 3 };
class SideEffect {
Collection Get() {
Console.Write("Get ");
return new Collection();
}
void Use() {
int i = Get()[^1];
Console.WriteLine(i);
}
}
Este valor se volverá a usar en el cálculo del segundo Slice argumento. Al hacerlo, se hará referencia a él como
start . El segundo argumento de Slice se obtendrá mediante la conversión de la expresión con tipo de
intervalo de la siguiente manera:
Cuando expr tiene el formato (donde se puede omitir) y tiene el tipo , se expr1..expr2 expr1 expr2 int
emitirá como expr2 - start .
Cuando expr tiene el formato expr1..^expr2 (donde se puede expr1 omitir), se emitirá como
(receiver.Length - expr2) - start .
Cuando expr tiene el formato expr1.. (donde se puede expr1 omitir), se emitirá como
receiver.Length - start .
De lo contrario, se emitirá como expr.End.GetOffset(receiver.Length) - start .
Las receiver Length expresiones , y se desbordarán según corresponda para asegurarse de que los efectos
secundarios solo se expr ejecutan una vez. Por ejemplo:
class Collection {
private int[] _array = new[] { 1, 2, 3 };
class SideEffect {
Collection Get() {
Console.Write("Get ");
return new Collection();
}
void Use() {
var array = Get()[0..2];
Console.WriteLine(array.Length);
}
}
Alternativas
Los nuevos operadores ( ^ y ) son el nivel de sugar .. sintáctico. La funcionalidad se puede implementar
mediante llamadas explícitas a los métodos de generador y , pero dará lugar a un código mucho más
reutilizable y la experiencia no será System.Index System.Range específica.
Representación de IL
Estos dos operadores se reducirán a llamadas regulares de indexador o método, sin cambios en las capas del
compilador posteriores.
Consideraciones
Detección de indexables basados en ICollection
La inspiración de este comportamiento fueron los inicializadores de colección. Usar la estructura de un tipo para
transmitir que ha optado por una característica. En el caso de los inicializadores de colección, los tipos pueden
participar en la característica implementando la interfaz IEnumerable (no genérica).
Esta propuesta requiere inicialmente que los tipos ICollection implementen para calificarse como indexables.
Sin embargo, esto requiere una serie de casos especiales:
ref struct : no pueden implementar interfaces, pero tipos como Span<T> son ideales para la compatibilidad
con índices o intervalos.
string : no implementa y ICollection agrega que tiene un gran interface costo.
Esto significa que ya se necesitan mayúsculas y minúsculas especiales para los tipos clave. El uso de mayúsculas
y minúsculas especial de es menos interesante, ya que el lenguaje lo hace en otras áreas string (reducción,
foreach constantes, etc.). El uso de mayúsculas y minúsculas especial de es más importante, ya que es un uso
especial de ref struct mayúsculas y minúsculas de una clase completa de tipos. Se etiquetan como indexables
si simplemente tienen una propiedad denominada Count con un tipo de valor devuelto de int .
Después de considerar el diseño se normalizó para decir que cualquier tipo que tenga una propiedad con un
Count / Length tipo de valor devuelto int de es Indexable. Esto quita todas las mayúsculas y minúsculas
especiales, incluso para string las matrices y .
Detectar solo recuento
La detección en los nombres Count de propiedad o complica un poco el Length diseño. Elegir solo uno para
estandarizar no es suficiente, ya que termina excluyendo un gran número de tipos:
Use Length : excluye prácticamente todas las colecciones de System.Collections y sub namespaces. Tienden
a derivar de ICollection y, por tanto, prefieren Count la longitud.
Use : excluye , matrices y Count la mayoría de los tipos string Span<T> ref struct basados
La complicación adicional en la detección inicial de tipos indexables se compensa por su simplificación en otros
aspectos.
Elección del segmento como nombre
Se ha elegido el nombre, ya que es el nombre estándar de facto para las operaciones de estilo Slice de
segmento en .NET. A partir de netcoreapp2.1, todos los tipos de estilo de intervalo usan el nombre Slice para
las operaciones de cortar. Antes de netcoreapp2.1, en realidad no hay ningún ejemplo de la sección para buscar
un ejemplo. Tipos como , , habría sido ideal para lalicación, pero el concepto no existía List<T>
ArraySegment<T> cuando se SortedList<T> agregaron tipos.
Las expresiones y se desbordarán según corresponda para asegurarse de que los efectos secundarios solo
receiver Length se ejecuten una vez. Por ejemplo:
class Collection {
private int[] _array = new[] { 1, 2, 3 };
class SideEffect {
Collection Get() {
Console.Write("Get ");
return new Collection();
}
void Use() {
int i = Get().GetAt(^1);
Console.WriteLine(i);
}
}
Este código imprimirá "Get Length 3".
Esta característica sería beneficiosa para cualquier miembro que tuviera un parámetro que representara un
índice. Por ejemplo, List<T>.InsertAt . Esto también tiene la posibilidad de confusión, ya que el lenguaje no
puede proporcionar ninguna guía sobre si una expresión está pensada o no para la indexación. Todo lo que
puede hacer es convertir cualquier Index expresión en al invocar un miembro en un tipo int Countable.
Restricciones:
Esta conversión solo es aplicable cuando la expresión con tipo Index es directamente un argumento para el
miembro. No se aplicaría a ninguna expresión anidada.
Reuniones de diseño
10 de enero de 2018
18 de enero de 2018
22 de enero de 2018
3 de diciembre de 2018
25 de marzo de 2019
1 de abril de 2019
15 de abril de 2019
"pattern-based using" y "using declarations"
18/09/2021 • 7 minutes to read
Resumen
El lenguaje agregará dos nuevas funcionalidades en torno a la instrucción para que la administración de
recursos sea más sencilla: debe reconocer un patrón descartable además de y agregar una declaración using
using al IDisposable using lenguaje.
Motivación
La instrucción es una herramienta eficaz para la administración de recursos en la actualidad, pero requiere
using bastantes esfuerzos. Los métodos que tienen una serie de recursos que se pueden administrar se pueden
etiquetar sintácticamente con una serie using de instrucciones. Esta carga de sintaxis es suficiente para que la
mayoría de las directrices de estilo de codificación tengan explícitamente una excepción en torno a las llaves
para este escenario.
La declaración quita gran parte del evento aquí y pone A C# a la par que otros using lenguajes que incluyen
bloques de administración de recursos. Además, la basada en patrones using permite a los desarrolladores
expandir el conjunto de tipos que pueden participar aquí. En muchos casos, se elimina la necesidad de crear
tipos de contenedor que solo existen para permitir el uso de valores en una using instrucción .
Juntas, estas características permiten a los desarrolladores simplificar y expandir los escenarios en los using
que se pueden aplicar.
Diseño detallado
declaración using
El idioma permitirá using agregarse a una declaración de variable local. Dicha declaración tendrá el mismo
efecto que declarar la variable en una using instrucción en la misma ubicación.
if (...)
{
using FileStream f = new FileStream(@"C:\users\jaredpar\using.md");
// statements
}
// Equivalent to
if (...)
{
using (FileStream f = new FileStream(@"C:\users\jaredpar\using.md"))
{
// statements
}
}
La duración de using un local se extenderá hasta el final del ámbito en el que se declara. A using continuación,
las locales se eliminarán en el orden inverso en el que se declaran.
{
using var f1 = new FileStream("...");
using var f2 = new FileStream("..."), f3 = new FileStream("...");
...
// Dispose f3
// Dispose f2
// Dispose f1
}
No hay ninguna restricción en torno a , ni a ninguna otra construcción de goto flujo de control en el caso de
una using declaración. En su lugar, el código actúa igual que lo haría para la instrucción using equivalente:
{
using var f1 = new FileStream("...");
target:
using var f2 = new FileStream("...");
if (someCondition)
{
// Causes f2 to be disposed but has no effect on f1
goto target;
}
}
Un valor local declarado en una using declaración local será implícitamente de solo lectura. Esto coincide con el
comportamiento de las locales declaradas en una using instrucción .
La gramática del lenguaje using para las declaraciones será la siguiente:
local-using-declaration:
using type using-declarators
using-declarators:
using-declarator
using-declarators , using-declarator
using-declarator:
identifier = expression
Esto permitirá a los desarrolladores using aprovechar en una serie de escenarios nuevos:
ref struct : estos tipos no pueden implementar interfaces hoy en día y, por tanto, no pueden participar en
using instrucciones .
Los métodos de extensión permitirán a los desarrolladores aumentar los tipos de otros ensamblados para
participar en using instrucciones .
{
Resource r = new Resource();
try {
// statements
}
finally {
if (r != null) r.Dispose();
}
}
Para ajustarse al patrón descartable, el método debe ser Dispose accesible, sin parámetros y tener un void
tipo de valor devuelto. No hay otras restricciones. Esto significa explícitamente que los métodos de extensión se
pueden usar aquí.
Consideraciones
etiquetas case sin bloques
Un using declaration no es posible directamente dentro de una etiqueta debido a case complicaciones en
torno a su duración real. Una posible solución es simplemente darle la misma duración que en out var la
misma ubicación. Se consideró que la complejidad adicional de la implementación de la característica y la
facilidad del trabajo (simplemente agregar un bloque a la etiqueta) no justificaba tomar case esta ruta.
Expansiones futuras
locales fijos
Una fixed instrucción tiene todas las propiedades de las instrucciones que using motivaron la capacidad de
tener using locales. También se debe tener en cuenta la ampliación de esta característica fixed a las locales.
Las reglas de duración y ordenación deben aplicarse igual de bien para using y fixed aquí.
Funciones locales estáticas
18/09/2021 • 2 minutes to read
Resumen
Admitir funciones locales que no permiten capturar el estado del ámbito de inclusión.
Motivación
Evite capturar involuntariamente el estado del contexto envolvente. Permite usar funciones locales en escenarios
donde static se requiere un método.
Diseño detallado
Una función local declarada static no puede capturar el estado del ámbito de inclusión. Como resultado, las
variables locales, los parámetros y this el ámbito de inclusión no están disponibles en una static función
local.
Una static función local no puede hacer referencia a miembros de instancia desde una referencia implícita o
explícita this o base .
Una static función local puede hacer referencia a static los miembros del ámbito de inclusión.
Una static función local puede hacer referencia a constant definiciones del ámbito de inclusión.
nameof() en una static función local, puede hacer referencia a variables locales, parámetros o this base en
el ámbito de inclusión.
Las reglas de accesibilidad para private los miembros del ámbito de inclusión son las mismas para static
static las funciones y no locales.
Una static definición de función local se emite como un static método en los metadatos, incluso si solo se
usa en un delegado.
Una función no static local o una expresión lambda puede capturar el estado de una static función local
envolvente, pero no puede capturar el estado fuera de la static función local envolvente.
static No se puede invocar una función local en un árbol de expresión.
Una llamada a una función local se emite como call en lugar de callvirt , independientemente de si la
función local es static .
Resolución de sobrecarga de una llamada en una función local que no se ve afectada por si la función local es
static .
Al quitar el static modificador de una función local en un programa válido, no se cambia el significado del
programa.
Design Meetings
https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-09-10.md#static-local-functions
Asignación de uso combinado de NULL
18/09/2021 • 4 minutes to read
[x] propuesto
[x] prototipo: completado
[x] implementación: completado
[x] Especificación: abajo
Resumen
Simplifica un patrón de codificación común en el que se asigna un valor a una variable si es NULL.
Como parte de esta propuesta, también se van a aflojar los requisitos de tipo en ?? para permitir que una
expresión cuyo tipo sea un parámetro de tipo no restringido se use en el lado izquierdo.
Motivación
Es habitual ver el código del formulario
if (variable == null)
{
variable = expression;
}
Esta propuesta agrega un operador binario no sobrecargable al lenguaje que realiza esta función.
Hay al menos ocho solicitudes de comunidad independientes para esta característica.
Diseño detallado
Agregamos un nuevo formulario de operador de asignación
assignment_operator
: '??='
;
Que sigue las reglas semánticas existentes para los operadores de asignación compuesta, salvo que Elide la
asignación si el lado izquierdo no es NULL. Las reglas para esta característica son las siguientes.
Dado a ??= b , donde A es el tipo de a , B es el tipo de b y A0 es el tipo subyacente de A si A es un tipo
de valor que acepta valores NULL:
1. Si no A existe o es un tipo de valor que no acepta valores NULL, se produce un error en tiempo de
compilación.
2. Si B no se pueden convertir implícitamente a A o A0 (si A0 existe), se produce un error en tiempo de
compilación.
3. Si A0 existe y B se pueden convertir implícitamente a A0 , y B no es dinámico, el tipo de a ??= b es A0
. a ??= b se evalúa en tiempo de ejecución como:
var tmp = a.GetValueOrDefault();
if (!a.HasValue) { tmp = b; a = tmp; }
tmp
1. Si existe y no es un tipo que acepta valores NULL o un tipo de referencia, se produce un error en tiempo
de compilación.
Inconvenientes
Al igual que con cualquier característica de lenguaje, debemos cuestionar si se reembolsa la complejidad
adicional del lenguaje en la claridad adicional que se ofrece al cuerpo de los programas de C# que se
beneficiarían de la característica.
Alternativas
El programador puede escribir (x = x ?? y) , if (x == null) x = y; o x ?? (x = y) a mano.
Preguntas no resueltas
[] Requiere la revisión de LDM
[] ¿También se deben admitir los &&= ||= operadores y?
Design Meetings
Ninguno.
Miembros de instancia de solo lectura
18/09/2021 • 8 minutes to read
Resumen
Proporcionar una manera de especificar miembros de instancia individuales en un struct no modifique el
estado, de la misma manera que especifica que ningún miembro de instancia readonly struct modifique el
estado.
Merece la pena tener en cuenta readonly instance member que != pure instance member . Un pure miembro de
instancia garantiza que no se modificará ningún estado. Un readonly miembro de instancia solo garantiza que
no se modificará el estado de la instancia.
Todos los miembros de instancia readonly struct de se podrían considerar implícitamente
readonly instance members . Las estructuras explícitas declaradas en estructuras que no son de solo lectura
readonly instance members se comportarán de la misma manera. Por ejemplo, crearían copias ocultas si llama a
un miembro de instancia (en la instancia actual o en un campo de la instancia) que no era de solo lectura.
Motivación
En la actualidad, los usuarios tienen la capacidad de crear tipos que el compilador exige que todos los campos
sean de solo lectura (y, por extensión, que ningún miembro de instancia readonly struct modifique el estado).
Sin embargo, hay algunos escenarios en los que tiene una API existente que expone campos accesibles o que
tiene una combinación de miembros mutados y no mutados. En estas circunstancias, no puede marcar el tipo
como readonly (sería un cambio importante).
Normalmente, esto no tiene mucho impacto, excepto en el caso de los in parámetros. Con los parámetros para
structs que no son de solo lectura, el compilador realizará una copia del parámetro para cada invocación de
miembro de instancia, ya que no puede garantizar que la invocación no modifique el in estado interno. Esto
puede provocar una gran cantidad de copias y un rendimiento general peor que si hubiera pasado la estructura
directamente por valor. Para obtener un ejemplo, vea este código en sharplab.
Algunos otros escenarios en los que se pueden producir copias ocultas son static readonly fields y literals
. Si se admiten en el futuro, terminarán en el mismo contenedor; es decir, todos necesitan actualmente una copia
completa (en la invocación de miembro de instancia) si la estructura no está blittable constants marcada
readonly como .
Diseño
Permitir que un usuario especifique que un miembro de instancia es, en sí mismo, y no modifica el estado de la
instancia (con toda la comprobación adecuada realizada por el readonly compilador, por supuesto). Por
ejemplo:
public struct Vector2
{
public float x;
public float y;
return tmp;
}
return vector.GetLength();
}
return vector.GetLengthReadonly();
}
}
Readonly se puede aplicar a los accessors de propiedad para indicar que this no se mutará en el accessor. Los
ejemplos siguientes tienen valores de solo lectura porque esos accessors modifican el estado del campo de
miembro, pero no modifican el valor de ese campo de miembro.
public readonly int Prop1
{
get
{
return this._store["Prop1"];
}
set
{
this._store["Prop1"] = value;
}
}
Cuando readonly se aplica a la sintaxis de propiedad, significa que todos los accessors son readonly .
Readonly solo se puede aplicar a los accessors que no mutan el tipo que lo contiene.
Readonly se puede aplicar a algunas propiedades implementadas automáticamente, pero no tendrá un efecto
significativo. El compilador tratará todos los getters implementados automáticamente como de solo lectura,
independientemente de si la readonly palabra clave está presente o no.
// Allowed
public readonly int Prop4 { get; }
public int Prop5 { readonly get; }
public int Prop6 { readonly get; set; }
// Not allowed
public readonly int Prop7 { get; set; }
public int Prop8 { get; readonly set; }
Readonly se puede aplicar a eventos implementados manualmente, pero no a eventos de tipo campo. Readonly
no se puede aplicar a los accessors de eventos individuales (agregar o quitar).
// Allowed
public readonly event Action<EventArgs> Event1
{
add { }
remove { }
}
// Not allowed
public readonly event Action<EventArgs> Event2;
public event Action<EventArgs> Event3
{
readonly add { }
readonly remove { }
}
public static readonly event Event4
{
add { }
remove { }
}
El compilador emitiría el miembro de instancia, como de costumbre, y emitiría además un atributo reconocido
por el compilador que indica que el miembro de instancia no modifica el estado. Esto hace que el parámetro
this oculto se convierta en en lugar de in T ref T .
Esto permitiría al usuario llamar de forma segura a dicho método de instancia sin que el compilador necesite
realizar una copia.
Entre las restricciones se incluyen:
El readonly modificador no se puede aplicar a métodos estáticos, constructores o destructores.
El readonly modificador no se puede aplicar a los delegados.
El readonly modificador no se puede aplicar a miembros de clase o interfaz.
Inconvenientes
Los mismos inconvenientes que existen actualmente con readonly struct los métodos. Cierto código puede
seguir causando copias ocultas.
Notas
También puede ser posible usar un atributo u otra palabra clave.
Esta propuesta está algo relacionada con (pero es más un subconjunto de) o , ambas con functional purity
constant expressions algunas propuestas existentes.
Permitir stackalloc en contextos anidados
18/09/2021 • 2 minutes to read
Asignación de pila
Se modifica la sección asignación de la pila de la especificación del lenguaje C# para relajar los lugares en los
stackalloc que puede aparecer una expresión. Eliminamos
local_variable_initializer_unsafe
: stackalloc_initializer
;
stackalloc_initializer
: 'stackalloc' unmanaged_type '[' expression ']'
;
y reemplácelas por
primary_no_array_creation_expression
: stackalloc_initializer
;
stackalloc_initializer
: 'stackalloc' unmanaged_type '[' expression? ']' array_initializer?
| 'stackalloc' '[' expression? ']' array_initializer
;
Tenga en cuenta que la adición de una array_initializer a stackalloc_initializer (y que la expresión de índice es
opcional) era una extensión en C# 7,3 y no se describe aquí.
El tipo de elemento de la stackalloc expresión es el unmanaged_type denominado en la expresión stackalloc, si
existe, o el tipo común entre los elementos de la array_initializer de lo contrario.
El tipo de stackalloc_initializer con el tipo de elemento K depende de su contexto sintáctico:
Si el stackalloc_initializer aparece directamente como local_variable_initializer de una instrucción de
local_variable_declaration o un for_initializer, su tipo es K* .
De lo contrario, su tipo es System.Span<K> .
Conversión stackalloc
La conversión stackalloc es una nueva conversión implícita integrada de la expresión. Cuando el tipo de una
stackalloc_initializer es K* , hay una conversión stackalloc implícita del stackalloc_initializer al tipo
System.Span<K> .
Registros
18/09/2021 • 26 minutes to read
record_declaration
: attributes? class_modifier* 'partial'? 'record' identifier type_parameter_list?
parameter_list? record_base? type_parameter_constraints_clause* record_body
;
record_base
: ':' class_type argument_list?
| ':' interface_type_list
| ':' class_type argument_list? ',' interface_type_list
;
record_body
: '{' class_member_declaration* '}' ';'?
| ';'
;
Los tipos de registro son tipos de referencia, similares a una declaración de clase. Es un error que un registro
proporcione si record_base argument_list no contiene record_declaration parameter_list . Como máximo,
una declaración de tipo parcial de un registro parcial puede proporcionar un parameter_list .
Los parámetros de registro no pueden usar modificadores o ref out this (pero in se permiten y params ).
Herencia
Los registros no pueden heredar de clases, a menos que la clase sea object y las clases no puedan heredar de
los registros. Los registros pueden heredar de otros registros.
La propiedad se puede declarar explícitamente. Es un error si la declaración explícita no coincide con la firma o
accesibilidad esperada, o si la declaración explícita no permite reemplazarla en un tipo derivado y el tipo de
registro no es sealed . Es un error si la propiedad sintetizada o declarada explícitamente no invalida una
propiedad con esta firma en el tipo de registro (por ejemplo, si falta la propiedad en , o Base Base sealed, o no
virtual, etc.). La propiedad sintetizada devuelve typeof(R) donde es el tipo de R registro.
El tipo de registro implementa e incluye una sobrecarga System.IEquatable<R> sintetizada fuertemente typed de
Equals(R? other) donde es el tipo de R registro. El método es y el método es a public menos que el tipo de
registro sea virtual sealed . El método se puede declarar explícitamente. Es un error si la declaración explícita
no coincide con la firma o accesibilidad esperada, o la declaración explícita no permite reemplazarla en un tipo
derivado y el tipo de registro no es sealed .
Si Equals(R? other) está definido por el usuario (no sintetizado), pero no lo GetHashCode está, se genera una
advertencia.
El sintetizado Equals(R?) devuelve si y solo si cada uno de los siguientes son true true :
other no es null , y
Para cada campo de instancia del tipo de registro que no se hereda, el valor fieldN de where es el tipo de
campo System.Collections.Generic.EqualityComparer<TN>.Default.Equals(fieldN, other.fieldN) TN y
Si hay un tipo de registro base, el valor de (una llamada no virtual a ); de lo base.Equals(other)
public virtual bool Equals(Base? other) contrario, el valor de EqualityContract == other.EqualityContract .
El tipo de registro incluye operadores == sintetizados != y equivalentes a los operadores declarados como
sigue:
Es un error si la invalidación se declara explícitamente. Es un error si el método no invalida (por ejemplo, debido
al sombreado en tipos object.Equals(object? obj) base intermedios, etc.). La invalidación sintetizada devuelve
Equals(other as R) donde es el tipo de R registro.
El tipo de registro incluye una invalidación sintetizada equivalente a un método declarado de la siguiente
manera:
Para esos tipos de registro, los miembros de igualdad sintetizados serían algo parecido a lo siguiente:
class R1 : IEquatable<R1>
{
public T1 P1 { get; init; }
protected virtual Type EqualityContract => typeof(R1);
public override bool Equals(object? obj) => Equals(obj as R1);
public virtual bool Equals(R1? other)
{
return !(other is null) &&
EqualityContract == other.EqualityContract &&
EqualityComparer<T1>.Default.Equals(P1, other.P1);
}
public static bool operator==(R1? left, R1? right)
=> (object)left == right || (left?.Equals(right) ?? false);
public static bool operator!=(R1? left, R1? right)
=> !(left == right);
public override int GetHashCode()
{
return Combine(EqualityComparer<Type>.Default.GetHashCode(EqualityContract),
EqualityComparer<T1>.Default.GetHashCode(P1));
}
}
Si el registro no tiene miembros imprimibles, el método llama al método base con PrintMembers un argumento
(su parámetro) y devuelve el builder resultado.
De lo contrario, el método :
1. llama al método base PrintMembers con un argumento (su parámetro builder ),
2. si el PrintMembers método devolvió true, anexe ", " al generador,
3. para cada uno de los miembros imprimibles del registro, anexa el nombre de ese miembro seguido de " = "
seguido del valor del miembro: (o para los tipos de valor), separados por this.member
this.member.ToString() ", ",
4. devuelve true.
El PrintMembers método se puede declarar explícitamente. Es un error si la declaración explícita no coincide con
la firma o accesibilidad esperada, o si la declaración explícita no permite invalidarla en un tipo derivado y el tipo
de registro no es sealed .
El registro incluye un método sintetizado equivalente a un método declarado de la siguiente manera:
El método se puede declarar explícitamente. Es un error si la declaración explícita no coincide con la firma o
accesibilidad esperada, o si la declaración explícita no permite reemplazarla en un tipo derivado y el tipo de
registro no es sealed . Es un error si el método sintetizado o declarado explícitamente no invalida (por ejemplo,
debido a la sombra en tipos object.ToString() base intermedios, etc.).
Método sintetizado:
1. crea una StringBuilder instancia de ,
2. anexa el nombre del registro al generador, seguido de " { ",
3. invoca el método del registro PrintMembers que le da el generador, seguido de "" si devuelve true,
4. anexa "}",
5. devuelve el contenido del generador con builder.ToString() .
Para esos tipos de registro, los miembros de impresión sintetizados serían algo parecido a lo siguiente:
class R1 : IEquatable<R1>
{
public T1 P1 { get; init; }
return true;
}
if (PrintMembers(builder))
builder.Append(" ");
builder.Append("}");
return builder.ToString();
}
}
builder.Append(nameof(P2));
builder.Append(" = ");
builder.Append(this.P2); // or builder.Append(this.P2); if P2 has a value type
builder.Append(", ");
builder.Append(nameof(P3));
builder.Append(" = ");
builder.Append(this.P3); // or builder.Append(this.P3); if P3 has a value type
return true;
}
if (PrintMembers(builder))
builder.Append(" ");
builder.Append("}");
return builder.ToString();
}
}
with_expression
: switch_expression
| switch_expression 'with' '{' member_initializer_list? '}'
;
member_initializer_list
: member_initializer (',' member_initializer)*
;
member_initializer
: identifier '=' expression
;
[x] propuesto
[x] prototipo: iniciado
[x] implementación: iniciada
[] Especificación: no iniciada
Resumen
Permita que una secuencia de instrucciones se produzca justo antes de la namespace_member_declaration de
un compilation_unit (es decir, un archivo de código fuente).
La semántica es que, si dicha secuencia de instrucciones está presente, se emitirá la siguiente declaración de
tipos, módulo el nombre de tipo real y el nombre del método:
Motivación
Hay una determinada cantidad de reutilización que se encuentra en la parte más sencilla de los programas,
debido a la necesidad de un Main método explícito. Esto parece ser un aprendizaje lingüístico y una claridad de
los programas. Por lo tanto, el objetivo principal de la característica es permitir programas de C# sin
reutilizaciones innecesarias en torno a ellos, por lo que los aprendidos y la claridad del código.
Diseño detallado
Sintaxis
La única sintaxis adicional es permitir una secuencia de instrucciones en una unidad de compilación, justo antes
de la namespace_member_declaration s:
compilation_unit
: extern_alias_directive* using_directive* global_attributes? statement* namespace_member_declaration*
;
Semántica
Si las instrucciones de nivel superior están presentes en cualquier unidad de compilación del programa, el
significado es como si se combinaran en el cuerpo del bloque de un Main método de una Program clase en el
espacio de nombres global, como se indica a continuación:
Tenga en cuenta que los nombres "Program" y "Main" se usan solo con fines ilustrativos, los nombres reales
usados por el compilador dependen de la implementación y no se puede hacer referencia al método por el
nombre del código fuente.
El método se designa como el punto de entrada del programa. Los métodos declarados explícitamente que por
Convención se pueden considerar como candidatos de punto de entrada se omiten. Cuando esto sucede, se
genera una advertencia. Es un error especificar el -main:<type> modificador del compilador cuando hay
instrucciones de nivel superior.
El método de punto de entrada siempre tiene un parámetro formal, string[] args . El entorno de ejecución
crea y pasa un string[] argumento que contiene los argumentos de la línea de comandos que se especificaron
cuando se inició la aplicación. El string[] argumento nunca es null, pero puede tener una longitud de cero si
no se especificó ningún argumento de línea de comandos. El parámetro ' args ' está en el ámbito dentro de las
instrucciones de nivel superior y no está en el ámbito fuera de ellos. Se aplican reglas de sombra o conflictos de
nombres normales.
Las operaciones asincrónicas se permiten en las instrucciones de nivel superior hasta el grado en que se
permiten en las instrucciones de un método de punto de entrada asincrónico normal. Sin embargo, no son
necesarios, si se await omiten las expresiones y otras operaciones asincrónicas, no se genera ninguna
advertencia.
La firma del método de punto de entrada generado se determina en función de las operaciones que utilizan las
instrucciones de nivel superior de la manera siguiente:
await System.Threading.Tasks.Task.Delay(1000);
System.Console.WriteLine("Hi!");
produciría:
await System.Threading.Tasks.Task.Delay(1000);
System.Console.WriteLine("Hi!");
return 0;
produciría:
produciría:
Sintaxis
Tipos de referencia que aceptan valores NULL y parámetros de tipo que aceptan valores NULL
Los tipos de referencia que aceptan valores NULL y los parámetros de tipo que aceptan valores NULL tienen la
misma sintaxis T? que la forma abreviada de tipos de valor que aceptan valores NULL, pero no tienen una
forma larga correspondiente.
Para los fines de la especificación, nullable_type se cambia el nombre de la producción actual a y
nullable_value_type nullable_reference_type se agregan las producciones:
nullable_type_parameter
type
: value_type
| reference_type
| nullable_type_parameter
| type_parameter
| type_unsafe
;
reference_type
: ...
| nullable_reference_type
;
nullable_reference_type
: non_nullable_reference_type '?'
;
non_nullable_reference_type
: reference_type
;
nullable_type_parameter
: non_nullable_non_value_type_parameter '?'
;
non_nullable_non_value_type_parameter
: type_parameter
;
primary_constraint
: ...
| 'class' '?'
;
Se debe crear una instancia de un parámetro de tipo restringido con class (en un contexto de anotación
habilitado ) con un tipo de referencia que no acepte valores NULL.
Se puede crear una instancia de un parámetro de tipo restringido con class? (o class en un contexto de
anotación deshabilitado ) con un tipo de referencia que acepte valores NULL o que no acepte valores NULL.
Se proporciona una advertencia en una class? restricción en un contexto de anotación deshabilitado .
notnull restricción
Un parámetro de tipo restringido con notnull no puede ser un tipo que acepta valores NULL (tipo de valor que
acepta valores NULL, tipo de referencia que acepta valores NULL o parámetro de tipo que acepta valores NULL).
primary_constraint
: ...
| 'notnull'
;
default restricción
La default restricción se puede usar en una invalidación de método o en una implementación explícita para
eliminar la ambigüedad que T? significa "parámetro de tipo que acepta valores NULL" de "tipo de valor que
acepta valores NULL" ( Nullable<T> ). Si falta la default restricción T? , una sintaxis en una implementación
de invalidación o explícita se interpretará como Nullable<T>
Consulta https://github.com/dotnet/csharplang/blob/master/proposals/csharp-9.0/unconstrained-type-
parameter-annotations.md#default-constraint.
El operador null-permisivo
El operador posterior a la corrección ! se denomina operador permisivo nulo. Se puede aplicar en un
primary_expression o en un null_conditional_expression:
primary_expression
: ...
| null_forgiving_expression
;
null_forgiving_expression
: primary_expression '!'
;
null_conditional_expression
: primary_expression null_conditional_operations_no_suppression suppression?
;
null_conditional_operations_no_suppression
: null_conditional_operations? '?' '.' identifier type_argument_list?
| null_conditional_operations? '?' '[' argument_list ']'
| null_conditional_operations '.' identifier type_argument_list?
| null_conditional_operations '[' argument_list ']'
| null_conditional_operations '(' argument_list? ')'
;
null_conditional_operations
: null_conditional_operations_no_suppression suppression?
;
suppression
: '!'
;
Por ejemplo:
var v = expr!;
expr!.M();
_ = a?.b!.c;
pp_nullable
: whitespace? '#' whitespace? 'nullable' whitespace nullable_action (whitespace nullable_target)?
pp_new_line
;
nullable_action
: 'disable'
| 'enable'
| 'restore'
;
nullable_target
: 'warnings'
| 'annotations'
;
#pragma warning las directivas se expanden para permitir cambiar el contexto de advertencia que acepta valores
NULL:
pragma_warning_body
: ...
| 'warning' whitespace warning_action whitespace 'nullable'
;
Por ejemplo:
Nulabilidad de tipos
Un tipo determinado puede tener uno de tres nullabilities: desconocen, nonnullable y Nullable.
Los tipos que no aceptan valores NULL pueden producir advertencias si null se le asigna un valor potencial.
Sin embargo, los tipos desconocen y que aceptan valores NULL son "asignable null" y pueden tener null
valores asignados sin advertencias.
Los valores de desconocen y los tipos que no aceptan valores NULL se pueden desreferenciar o asignar sin
advertencias. Sin embargo, los valores de los tipos que aceptan valores NULL son "produjeron valores NULL" y
pueden provocar advertencias al desreferenciarse o asignarse sin una comprobación nula adecuada.
El estado predeterminado NULL de un tipo de rendimiento nulo es "quizás null" o "quizás default". El estado
Null predeterminado de un tipo de rendimiento que no es NULL es "not null".
El tipo de tipo y el contexto de anotación que acepta valores NULL que tiene lugar en determina su nulabilidad:
Un tipo de valor que no acepta valores NULL S siempre es no acepta valores NULL
Un tipo de valor que acepta valores NULL S? siempre admite valores NULL
Un tipo de referencia sin anotar C en un contexto de anotación deshabilitado es desconocen
Un tipo de referencia sin anotar C en un contexto de anotación habilitado no admite valores NULL
Un tipo de referencia que acepta valores NULL admite C? valores NULL (pero se puede producir una
advertencia en un contexto de anotación deshabilitado )
Los parámetros de tipo también tienen en cuenta las restricciones:
Parámetro de tipo T en el que todas las restricciones (si existen) son tipos que aceptan valores NULL o la
class? restricción admite valores NULL .
Un parámetro de tipo en T el que al menos una restricción es desconocen o que no admite valores NULL , o
una de las struct class restricciones o notnull es.
desconocen en un contexto de anotación deshabilitado
no acepta valores NULL en un contexto de anotación habilitado
Un parámetro de tipo que acepta valores NULL admite T? valores NULL, pero se produce una advertencia
en un contexto de anotación deshabilitado si T no es un tipo de valor
Desconocen frente a no Nullable
type Se considera que se produce en un contexto de anotación determinado cuando el último token del tipo
está dentro de ese contexto.
Si un tipo de referencia determinado C en el código fuente se interpreta como desconocen o no acepta valores
NULL, depende del contexto de la anotación de ese código fuente. Pero una vez establecido, se considera parte
de ese tipo y "viaja con él", por ejemplo, durante la sustitución de los argumentos de tipo genérico. Es como si
hubiera una anotación como ? en el tipo, pero no es visible.
Restricciones
Los tipos de referencia que aceptan valores NULL se pueden usar como restricciones genéricas.
class? es una nueva restricción que denota "posible tipo de referencia que acepta valores NULL", mientras que
class en un contexto de anotación habilitado denota "tipo de referencia que no acepta valores NULL".
default es una nueva restricción que denota un parámetro de tipo que no se sabe que es un tipo de referencia
o de valor. Solo se puede usar en métodos invalidados y implementados explícitamente. Con esta restricción,
T? significa un parámetro de tipo que acepta valores NULL, en lugar de ser un método abreviado para
Nullable<T> .
notnull es una nueva restricción que denota un parámetro de tipo que no acepta valores NULL.
La nulabilidad de un argumento de tipo o de una restricción no afecta a si el tipo satisface la restricción, excepto
en el caso de que ya sea el caso hoy (los tipos de valor que aceptan valores NULL no satisfacen la struct
restricción). Sin embargo, si el argumento de tipo no satisface los requisitos de nulabilidad de la restricción, se
puede proporcionar una advertencia.
// The value `t` here has the state "maybe null". It's possible for `T` to be instantiated
// with `string?` in which case `null` would be within the domain of legal values here. The
// assumption though is the value provided here is within the legal values of `T`. Hence
// if `T` is `string` then `null` will not be a value, just as we assume that `null` is not
// provided for a normal `string` parameter
void M<T>(T t)
{
// There is no guarantee that default(T) is within the legal values for T hence the
// state *must* be "maybe-default" and hence `local` must be `T?`
T? local = default(T);
}
void Use(string s)
{
// ...
}
Expresiones de invocación
Si invocation_expression invoca un miembro declarado con uno o varios atributos para un comportamiento
especial nulo, el estado NULL se determina mediante esos atributos. De lo contrario, el estado null de la
expresión es el estado Null predeterminado de su tipo.
invocation_expression El compilador no realiza el seguimiento del estado null de un.
Acceso a elementos
Si element_access invoca un indexador que se declara con uno o más atributos para un comportamiento
especial nulo, los atributos determinan el estado null. De lo contrario, el estado null de la expresión es el estado
Null predeterminado de su tipo.
object?[] array = ...;
if (array[0] != null)
{
// Warning: Converting null literal or possible null value to non-nullable type.
object o = array[0];
// Warning: Dereference of a possibly null reference.
Console.WriteLine(o.ToString());
}
Acceso base
Si B denota el tipo base del tipo envolvente, base.I tiene el mismo estado null que ((B)this).I y base[E]
tiene el mismo estado null que ((B)this)[E] .
Expresiones predeterminadas
default(T) tiene el estado null en función de las propiedades del tipo T :
Si el tipo es un tipo que no acepta valores NULL , tiene el estado null "not null"
De lo contrario, si el tipo es un parámetro de tipo, tiene el estado null "quizás default".
En caso contrario, tiene el estado null "quizás null"
Expresiones condicionales nulas?.
Un null_conditional_expression tiene el estado Null basado en el tipo de expresión. Tenga en cuenta que esto
hace referencia al tipo de null_conditional_expression , no al tipo original del miembro que se invoca:
Si el tipo es un tipo de valor que acepta valores NULL , tiene el estado null "maybe null"
De lo contrario, si el tipo es un parámetro de tipo que acepta valores NULL , tiene el estado null "quizás
default".
En caso contrario, tiene el estado null "quizás null"
Expresiones de conversión
Si una expresión de conversión (T)E invoca una conversión definida por el usuario, el estado null de la
expresión es el estado predeterminado NULL para el tipo de la conversión definida por el usuario. De lo
contrario:
Si Tes un tipo de valor que no acepta valores NULL , T tiene el estado null "not null"
T De lo contrario, si es un tipo de valor que acepta valores NULL , T tiene el estado null "maybe null"
De lo contrario T , si es un tipo que acepta valores NULL en el formulario U? U , donde es un parámetro
de tipo, T tiene el estado null "quizás default".
De lo contrario, si T es un tipo que acepta valores NULL y E tiene el estado null "maybe null" o "quizás
default", T tiene el estado null "maybe null"
De lo contrario, si T es un parámetro de tipo y E tiene el estado null "maybe null" o "quizás default", T
tiene el estado null "quizás default".
Else T tiene el mismo estado null que E
Operadores unarios y binarios
Si un operador unario o binario invoca a un operador definido por el usuario, el estado null de la expresión es el
estado predeterminado NULL para el tipo del operador definido por el usuario. De lo contrario, es el estado null
de la expresión.
¿Algo especial de hacer para binario + en cadenas y delegados?
Expresiones Await
El estado null de await E es el estado Null predeterminado de su tipo.
El operador as
El estado null de una E as T expresión depende primero de las propiedades del tipo T . Si el tipo de T no
admite valores NULL , el estado NULL es "not null". En caso contrario, el estado Null depende de la conversión
del tipo de E al tipo T :
Si la conversión es una identidad, una conversión boxing, una referencia implícita o una conversión implícita
que acepta valores NULL, el estado NULL es el estado null de E
T De lo contrario, si es un parámetro de tipo, tiene el estado null "quizás default".
En caso contrario, tiene el estado null "quizás null"
Operador de uso combinado de null
El estado null de E1 ?? E2 es el estado nulo de E2
El operador condicional
El estado null de E1 ? E2 : E3 se basa en el estado null de E2 y E3 :
Si ambos son "not null", el estado NULL es "not null"
De lo contrario, si es "quizás default", el estado NULL es "quizás default".
De lo contrario, el estado NULL es "not null"
Expresiones de consulta
El estado null de una expresión de consulta es el estado Null predeterminado de su tipo.
Trabajo adicional necesario aquí
Operadores de asignación
E1 = E2 y E1 op= E2 tienen el mismo estado null que E2 cuando se ha aplicado cualquier conversión
implícita.
Expresiones que propagan el estado null
(E)``checked(E) y unchecked(E) todos tienen el mismo estado null que E .
Expresiones que nunca son NULL
El estado null de los siguientes formatos de expresión siempre es "not null":
this access
cadenas interpoladas
new expresiones (expresiones de objeto, delegado, objeto anónimo y creación de matriz)
Expresiones typeof
Expresiones nameof
funciones anónimas (métodos anónimos y expresiones lambda)
Expresiones permisivo nulas
Expresiones is
Funciones anidadas
Las funciones anidadas (expresiones lambda y funciones locales) se tratan como métodos, excepto en lo que
respecta a las variables capturadas. El estado inicial de una variable capturada dentro de una función lambda o
local es la intersección del estado que acepta valores NULL de la variable en todos los "usos" de esa función
anidada o expresión lambda. El uso de una función local es una llamada a esa función, o bien, donde se
convierte en un delegado. El uso de una expresión lambda es el punto en el que se define en el origen.
Inferencia de tipos
variables locales con tipo implícito que aceptan valores NULL
var deduce un tipo anotado para los tipos de referencia y los parámetros de tipo que no están restringidos
para ser un tipo de valor. Por ejemplo:
en var s = ""; var , se deduce como string? .
en var t = new T(); con una sin restricciones T var , se deduce como T? .
Inferencia de tipo genérico
La inferencia de tipos genéricos se ha mejorado para ayudar a decidir si los tipos de referencia deducidos deben
admitir valores NULL o no. Se trata de un mejor esfuerzo. Puede producir advertencias con respecto a las
restricciones de nulabilidad y puede dar lugar a advertencias que aceptan valores NULL cuando los tipos
deducidos de la sobrecarga seleccionada se aplican a los argumentos.
La primera fase
Los tipos de referencia que aceptan valores NULL fluyen en los límites de las expresiones iniciales, como se
describe a continuación. Además, null se introducen dos nuevos tipos de límites, es decir, y default . Su
finalidad es llevar a cabo a través de null las apariciones de o default en las expresiones de entrada, lo que
puede hacer que un tipo deducido acepte valores NULL, aunque no lo sea. Esto funciona incluso para los tipos
de valor que aceptan valores NULL, que se han mejorado para recoger la "nulación" en el proceso de inferencia.
La determinación de los límites que se van a agregar en la primera fase se ha mejorado como se indica a
continuación:
Si un argumento Ei tiene un tipo de referencia, el tipo que se U usa para la inferencia depende del estado null
de, así Ei como de su tipo declarado:
Si el tipo declarado es un tipo de referencia que no admite valores NULL U0 o un tipo de referencia que
acepta valores NULL U0? , entonces
Si el estado null de Ei es "not null", entonces U``U0
Si el estado null de Ei es "quizás null", entonces U``U0?
De lo contrario Ei , si tiene un tipo declarado, U es de ese tipo.
De lo Ei contrario null , si es, U el enlace especial null
De lo Ei contrario default , si es, U el enlace especial default
En caso contrario, no se realiza ninguna inferencia.
Inferencias exactas, de límite superior y de límite inferior
En las inferencias del tipo U al tipo V , si V es un tipo de referencia que acepta valores NULL V0? , V0 se
usa en lugar de V en las cláusulas siguientes.
Si V es una de las variables de tipo sin corregir, U se agrega como un límite inferior, superior o inferior
como antes.
De lo contrario, si U es null o default , no se realiza ninguna inferencia.
De lo contrario, si U es un tipo de referencia que acepta valores NULL U0? , U0 se utiliza en lugar de U en
las cláusulas posteriores.
La esencia es que la nulabilidad que pertenece directamente a una de las variables de tipo sin Fixed se conserva
en sus límites. Por otra parte, para las inferencias que se recorren más en los tipos de origen y de destino, se
omite la nulabilidad. Puede o no coincidir, pero si no es así, se emitirá una advertencia más adelante si se elige la
sobrecarga y se aplica.
Corrección de
Actualmente, la especificación no realiza un buen trabajo de describir lo que sucede cuando varios límites son
convertibles entre sí, pero son diferentes. Esto puede ocurrir entre object y dynamic , entre tipos de tupla que
solo difieren en los nombres de elemento, entre los tipos construidos en él y ahora también entre C y C? para
los tipos de referencia.
Además, debemos propagar la "nulación" de las expresiones de entrada al tipo de resultado.
Para controlar estos agregamos más fases para corregir, que ahora es:
1. Recopile todos los tipos de todos los límites como candidatos, quitando ? de todos los tipos de referencia
que aceptan valores NULL
2. Eliminación de candidatos en función de los requisitos de los límites exactos, inferiores y superiores
(mantenimiento null y default límites)
3. Eliminación de candidatos que no tienen una conversión implícita a todos los demás candidatos
4. Si los candidatos restantes no tienen conversiones de identidad entre sí, se produce un error en la inferencia
de tipos
5. Combine los candidatos restantes como se describe a continuación
6. Si el candidato resultante es un tipo de referencia o un tipo de valor que no acepta valores NULL y todos los
límites exactos o alguno de los límites inferiores son tipos de valor que aceptan valores NULL, tipos de
referencia que aceptan valores NULL o null default , ? se agregan al candidato resultante, lo que lo
convierte en un tipo de valor que acepta valores NULL o un tipo de referencia.
La combinación se describe entre dos tipos candidatos. Es transitiva y conmutable, por lo que los candidatos se
pueden combinar en cualquier orden con el mismo resultado final. No se define si los dos tipos candidatos no
son una identidad convertible entre sí.
La función Merge toma dos tipos de candidatos y una dirección ( + o - ):
Merge( T , , d) = T
T
Merge( S T? , + ) = Merge( S? , T , + ) = Merge( S , T , + ) ?
,
Merge( S T? , - ) = Merge( S? , T , - ) = Merge( S , T , - )
,
Merge( C<S1,...,Sn> , C<T1,...,Tn> , + ) = C< Merge( S1 , T1 , D1 ) ,..., Merge( Sn , Tn , DN) > ,
donde
di = + Si el i parámetro de tipo TH de C<...> es covariante
di = - Si el i parámetro de tipo ' th of C<...> es compensa-or invariable
Merge( C<S1,...,Sn> , C<T1,...,Tn> , - ) = C< Merge( S1 , T1 , D1) ,..., Merge( Sn , Tn , DN) > ,
donde
di = - Si el i parámetro de tipo TH de C<...> es covariante
di = + Si el i parámetro de tipo ' th of C<...> es compensa-or invariable
Merge( (S1 s1,..., Sn sn) , (T1 t1,..., Tn tn) , d) = ( Merge( S1 , T1 , d) n1,..., Merge( Sn , Tn ,
d) nn) , donde
ni está ausente si si y ti difieren, o si ambos están ausentes
ni es si si si y ti son iguales.
Merge( object , dynamic ) = Merge( dynamic , object ) = dynamic
Advertencias
Posible asignación null
Desreferenciación potencial null
No coincide la nulabilidad de restricciones
Tipos que aceptan valores NULL en el contexto de anotación deshabilitado
Incoherencia en la nulabilidad de invalidación e implementación
Estamos considerando un pequeño conjunto de mejoras en la coincidencia de patrones para C# 9,0 que tienen
sinergia natural y funcionan bien para abordar varios problemas de programación comunes:
https://github.com/dotnet/csharplang/issues/2925 Patrones de tipos
https://github.com/dotnet/csharplang/issues/1350 Patrones entre paréntesis para aplicar o resaltar la
prioridad de los nuevos combinadores
https://github.com/dotnet/csharplang/issues/1350 and Patrones de condisyuntiva que requieren ambos
patrones diferentes para coincidir;
https://github.com/dotnet/csharplang/issues/1350 or Patrones imprecisos que requieren uno de dos
patrones diferentes para coincidir;
https://github.com/dotnet/csharplang/issues/1350 Modelos negados not que requieren que un patrón
determinado no coincidan; y
https://github.com/dotnet/csharplang/issues/812 Los modelos relacionales que requieren que el valor de
entrada sea menor que, menor o igual que, etc., una constante determinada.
primary_pattern
: parenthesized_pattern
| // all of the existing forms
;
parenthesized_pattern
: '(' pattern ')'
;
Patrones de tipos
Se permite un tipo como patrón:
primary_pattern
: type-pattern
| // all of the existing forms
;
type_pattern
: type
;
Esta retcons la existente is-Type-Expression para que sea una expresión-Pattern-Pattern en la que el patrón es un
patrón de tipo, aunque no cambiaremos el árbol de sintaxis generado por el compilador.
Un problema de implementación sutil es que esta gramática es ambigua. Una cadena como a.b puede
analizarse como un nombre completo (en un contexto de tipo) o una expresión con puntos (en un contexto de
expresión). El compilador ya es capaz de tratar un nombre completo igual que una expresión de puntos con el fin
de controlar algo como e is Color.Red . El análisis semántico del compilador se extendería aún más para poder
enlazar un patrón de constante (sintáctico) (por ejemplo, una expresión con puntos) como un tipo con el fin de
tratarlo como un patrón de tipo enlazado con el fin de admitir esta construcción.
Después de este cambio, podrá escribir
Modelos relacionales
Los modelos relacionales permiten al programador expresar que un valor de entrada debe cumplir una
restricción relacional en comparación con un valor constante:
Los modelos relacionales admiten los operadores relacionales < ,, <= > y >= en todos los tipos integrados
que admiten operadores relacionales binarios con dos operandos del mismo tipo en una expresión. En concreto,
se admiten todos estos modelos relacionales para sbyte , byte , short , ushort , int ,, uint long , ulong
, char ,,,, float double decimal nint y nuint .
primary_pattern
: relational_pattern
;
relational_pattern
: '<' relational_expression
| '<=' relational_expression
| '>' relational_expression
| '>=' relational_expression
;
La expresión debe evaluarse como un valor constante. Es un error si ese valor constante es double.NaN o
float.NaN . Es un error si la expresión es una constante nula.
Cuando la entrada es un tipo para el que se define un operador relacional binario integrado adecuado que es
aplicable a la entrada como operando izquierdo y la constante especificada como su operando derecho, la
evaluación de ese operador se toma como el significado del patrón relacional. De lo contrario, convertiremos la
entrada al tipo de la expresión usando una conversión explícita que acepta valores NULL o unboxing. Es un error
en tiempo de compilación si no existe tal conversión. Se considera que el patrón no coincide si se produce un
error en la conversión. Si la conversión se realiza correctamente, el resultado de la operación de coincidencia de
patrones es el resultado de evaluar la expresión, e OP v donde e es la entrada convertida, OP es el operador
relacional y v es la expresión constante.
Combinadores de patrones
Los combinadores de patrones permiten buscar coincidencias de dos patrones diferentes mediante and (esto
puede extenderse a cualquier número de patrones mediante el uso repetido de and ), uno de dos patrones
diferentes mediante or (Ditto) o la negación de un patrón mediante not .
Un uso común de un combinador será la expresión
Más legible que la expresión actual e is object , este patrón expresa claramente que se está comprobando si
hay un valor distinto de NULL.
Los and or combinadores y serán útiles para probar rangos de valores.
bool IsLetter(char c) => c is >= 'a' and <= 'z' or >= 'A' and <= 'Z';
Este ejemplo muestra que tendrá and una prioridad de análisis superior (es decir, se enlazará más
estrechamente) que or . El programador puede usar el patrón entre paréntesis para que la prioridad sea
explícita:
bool IsLetter(char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');
Al igual que todos los patrones, estos combinadores se pueden usar en cualquier contexto en el que se espera
un patrón, incluidos los patrones anidados, is-Pattern-Expression, la expresión switch y el patrón de la etiqueta
de caso de una instrucción switch.
pattern
: disjunctive_pattern
;
disjunctive_pattern
: disjunctive_pattern 'or' conjunctive_pattern
| conjunctive_pattern
;
conjunctive_pattern
: conjunctive_pattern 'and' negated_pattern
| negated_pattern
;
negated_pattern
: 'not' negated_pattern
| primary_pattern
;
primary_pattern
: // all of the patterns forms previously defined
;
Pero cuando la entrada no es un tipo primitivo, ¿en qué tipo se intenta convertirla?
Hemos propuesto que cuando el tipo de entrada ya sea un primitivo comparable, es decir, el tipo de la
comparación. Sin embargo, cuando la entrada no es un primitivo comparable, tratamos el relacional como una
prueba de tipo implícito para el tipo de la constante en el lado derecho del relacional. Si el programador intenta
admitir más de un tipo de entrada, debe realizarse explícitamente:
Se ha sugerido que al escribir un and combinador, la información de tipo aprendida a la izquierda sobre el tipo
de nivel superior podría fluir hacia la derecha. Por ejemplo
Aquí, el tipo de entrada para el segundo patrón se limita mediante los requisitos de restricción de tipo de la
izquierda de and . Definiremos la semántica de restricción de tipos para todos los patrones como se indica a
continuación. El tipo restringido de un patrón P se define de la siguiente manera:
1. Si Pes un patrón de tipo, el tipo restringido es el tipo del tipo de patrón de tipo.
2. Si P es un modelo de declaración, el tipo restringido es el tipo del tipo del modelo de declaración.
3. Si P es un patrón recursivo que proporciona un tipo explícito, el tipo restringido es ese tipo.
4. Si P coincide con las reglas de ITuple , el tipo restringido es el tipo
System.Runtime.CompilerServices.ITuple .
5. Si P es un patrón constante en el que la constante no es la constante NULL y donde la expresión no tiene
una conversión de expresión constante en el tipo de entrada, el tipo restringido es el tipo de la constante.
6. Si P es un patrón relacional en el que la expresión constante no tiene una conversión de expresión
constante en el tipo de entrada, el tipo restringido es el tipo de la constante.
7. Si P es un or patrón, el tipo restringido es el tipo común del tipo restringido de los subpatróns si existe
dicho tipo común. Para este propósito, el algoritmo de tipo común solo tiene en cuenta las conversiones de
referencia implícita, de identidad y de conversión boxing, y considera todos los subpatróns de una secuencia
de or patrones (omitiendo los patrones entre paréntesis).
8. Si P es un and patrón, el tipo restringido es el tipo restringido del patrón derecho. Además, el tipo
restringido del patrón izquierdo es el tipo de entrada del patrón de la derecha.
9. De lo contrario, el tipo restringido de P es el tipo de P entrada.
Definiciones de variables y asignación definitiva
La adición de or not patrones y crea algunos problemas interesantes sobre las variables de patrón y la
asignación definitiva. Puesto que las variables se pueden declarar normalmente como máximo una vez, parecería
que cualquier variable de patrón declarada en un lado de un or patrón no se asignaría definitivamente cuando
el patrón coincida. Del mismo modo, una variable declarada dentro de un not patrón no se espera que se
asigne definitivamente cuando el patrón coincide con. La manera más sencilla de solucionarlo es prohibir las
variables de patrón en estos contextos. Sin embargo, esto puede ser demasiado restrictivo. Hay otros enfoques a
tener en cuenta.
Un escenario que merece la pena considerar es este
Esto no funciona actualmente porque, para un is-Pattern-Expression, las variables de patrón se consideran
asignadas definitivamente , donde is-Pattern-Expression es true ("definitivamente asignada cuando es true").
Admitir esto sería más sencillo (desde la perspectiva del programador) que agregar también compatibilidad con
una instrucción de condición negada if . Incluso si agregamos esta compatibilidad, los programadores
preguntarían por qué el fragmento de código anterior no funciona. Por otro lado, el mismo escenario en una
switch tiene menos sentido, ya que no hay ningún punto correspondiente en el programa en el que se asigne
definitivamente cuando el valor false sea significativo. ¿Permitimos esto en una expresión de patrón is pero no
en otros contextos en los que se permiten patrones? Parece irregular.
Relacionado con esto es el problema de la asignación definitiva en un patrón disyuntiva.
if (e is 0 or int i)
{
M(i); // is i definitely assigned here?
}
Solo esperamos que i se asigne definitivamente cuando la entrada no sea cero. Pero como no sabemos si la
entrada es cero o no está dentro del bloque, i no se asigna definitivamente. Sin embargo, ¿qué ocurre si se
permite que i se declare en diferentes patrones mutuamente excluyentes?
Aquí, la variable i se asigna definitivamente dentro del bloque y toma el valor del otro elemento de la tupla
cuando se encuentra un elemento de cero.
También se ha sugerido para permitir que las variables se hayan definido (multiplicar) en todos los casos de un
bloque de casos:
Para realizar cualquiera de estas tareas, tendríamos que definir cuidadosamente dónde se permiten estas
múltiples definiciones y en qué condiciones una variable de este tipo se considera definitivamente asignada.
Si optamos por aplazar el trabajo hasta más tarde (lo que le aconsejo), podríamos decir en C# 9
debajo de not o or , es posible que las variables de patrón no se declaren.
A continuación, tendríamos tiempo para desarrollar una experiencia que proporcione información sobre el valor
posible de relajar más adelante.
Diagnósticos, supuestos y exhaustivos
Estos nuevos formatos de patrón presentan muchas oportunidades nuevas para el error de programador
diagnosticable. Tendremos que decidir qué tipos de errores se van a diagnosticar y cómo hacerlo. Estos son
algunos ejemplos:
Este caso nunca puede coincidir (porque la entrada no puede ser int ni double ). Ya tenemos un error cuando
se detecta un caso que nunca puede coincidir, pero su redacción ("el caso del conmutador ya se ha controlado
en un caso anterior" y "el patrón ya se ha controlado por un brazo anterior de la expresión switch") puede ser
engañoso en nuevos escenarios. Es posible que tenga que modificar el texto para indicar simplemente que el
patrón nunca coincidirá con la entrada.
case 1 and 2:
Del mismo modo, esto sería un error porque un valor no puede ser 1 y 2 .
case 1 or 2 or 3 or 1:
Este caso puede coincidir, pero al or 1 final no se agrega ningún significado al patrón. Se recomienda que se
produzca un error siempre que algún conjunción o disjunct de un patrón compuesto no defina una variable de
patrón ni afecte al conjunto de valores coincidentes.
Aquí, no 0 or 1 or se agrega nada al segundo caso, ya que esos valores se habrían controlado por el primer
caso. Esto también merece un error.
byte b = ...;
int x = b switch { <100 => 0, 100 => 1, 101 => 2, >101 => 3 };
Una expresión switch como esta debe considerarse exhaustiva (controla todos los valores de entrada posibles).
En C# 8,0, una expresión switch con una entrada de tipo byte solo se considera exhaustiva si contiene un brazo
final cuyo patrón coincide con todo (un patrón de descarte o un patrón var). Incluso una expresión switch que
tiene un brazo para cada byte valor distinto no se considera exhaustiva en C# 8. Con el fin de controlar de
forma adecuada la de los patrones relacionales, también tendremos que tratar este caso. Técnicamente, se trata
de un cambio importante, pero es probable que no se note ningún usuario.
Establecedores de solo inicialización
18/09/2021 • 30 minutes to read
Resumen
Esta propuesta agrega el concepto de propiedades e indizadores de solo inicialización a C#. Estas propiedades e
indexadores se pueden establecer en el momento de la creación del objeto, pero solo se convierten en get una
vez que se ha completado la creación del objeto. Esto permite un modelo inmutable mucho más flexible en C#.
Motivación
Los mecanismos subyacentes para generar datos inmutables en C# no han cambiado desde 1,0. Permanecen:
1. Declarar campos como readonly .
2. Declarar propiedades que contienen solo un get descriptor de acceso.
Estos mecanismos son eficaces para permitir la construcción de datos inmutables, pero lo hacen mediante la
adición de costos al código reutilizable de tipos y la aceptación de estos tipos de características como
inicializadores de objeto y colección. Esto significa que los desarrolladores deben elegir entre facilidad de uso e
inmutabilidad.
Un objeto simple inmutable como Point requiere dos veces el código de placa de la caldera para admitir la
construcción, como hace para declarar el tipo. Cuanto mayor sea el tipo, mayor será el costo de esta placa de
caldera:
struct Point
{
public int X { get; }
public int Y { get; }
El init descriptor de acceso hace que los objetos inmutables sean más flexibles, ya que permite al llamador
mutar los miembros durante la acción de construcción. Esto significa que las propiedades inmutables del objeto
pueden participar en los inicializadores de objeto y, por lo tanto, eliminan la necesidad de todo el constructor
reutilizable en el tipo. El Point tipo es ahora simplemente:
struct Point
{
public int X { get; init; }
public int Y { get; init; }
}
class Student
{
public string FirstName { get; init; }
public string LastName { get; init; }
}
Una propiedad de instancia que contiene un init descriptor de acceso se considera configurable en las
siguientes circunstancias, excepto cuando se encuentra en una función local o una expresión lambda:
Durante un inicializador de objeto
Durante un with inicializador de expresión
Dentro de un constructor de instancia del tipo que contiene o derivado, en this o base
Dentro del init descriptor de acceso de cualquier propiedad, en this o base
Uso de atributos con parámetros con nombre
Las horas anteriores en las que init se pueden establecer los descriptores de acceso se denominan
colectivamente en este documento como la fase de construcción del objeto.
Esto significa que la Student clase se puede utilizar de las siguientes maneras:
Las reglas que se aplican cuando los init descriptores de acceso se pueden configurar se amplían entre las
jerarquías de tipo Si el miembro es accesible y se sabe que el objeto está en la fase de construcción, se puede
establecer el miembro. Esto permite específicamente lo siguiente:
class Base
{
public bool Value { get; init; }
}
class Consumption
{
void Example()
{
var d = new Derived() { Value = true; };
}
}
En el punto en el que init se invoca a un descriptor de acceso, se sabe que la instancia está en la fase de
construcción abierta. Por lo tanto, init se permite que un descriptor de acceso realice las siguientes acciones,
además de lo que set puede hacer un descriptor de acceso normal:
1. Llamar a otros init descriptores de acceso disponibles mediante this o base
2. Asignar readonly campos declarados en el mismo tipo mediante this
class Complex
{
readonly int Field1;
int Field2;
int Prop1 { get; init ; }
int Prop2
{
get => 42;
init
{
Field1 = 13; // okay
Field2 = 13; // okay
Prop1 = 13; // okay
}
}
}
La capacidad de asignar readonly campos de un init descriptor de acceso se limita a los campos declarados
en el mismo tipo que el descriptor de acceso. No se puede usar para asignar readonly campos en un tipo base.
Esta regla garantiza que los autores de tipos permanecen en el control sobre el comportamiento de mutabilidad
de su tipo. Los desarrolladores que no desean usar no se pueden init ver afectados por otros tipos que elijan
hacerlo:
class Base
{
internal readonly int Field;
internal int Property
{
get => Field;
init => Field = value; // Okay
}
public Derived()
{
Property = 42; // Okay
Field = 13; // Error Field is readonly
}
}
Cuando init se utiliza en una propiedad virtual, todas las invalidaciones también se deben marcar como init
. Del mismo modo, no es posible invalidar un sencillo set con init .
class Base
{
public virtual int Property { get; init; }
}
class C1 : Base
{
public override int Property { get; init; }
}
class C2 : Base
{
// Error: Property must have init to override Base.Property
public override int Property { get; set; }
}
Una interface Declaración también puede participar en init la inicialización de estilo mediante el siguiente
patrón:
interface IPerson
{
string Name { get; init; }
}
class Init
{
void M<T>() where T : IPerson, new()
{
var local = new T()
{
Name = "Jared"
};
local.Name = "Jraed"; // Error
}
}
struct ReadonlyStruct2
{
public readonly int Prop2 { get; init; } // Allowed
Codificación de metadatos
init Los descriptores de acceso de propiedad se emitirán como un set descriptor de acceso estándar con el
tipo de valor devuelto marcado con un modreq de IsExternalInit . Se trata de un nuevo tipo que tendrá la
siguiente definición:
namespace System.Runtime.CompilerServices
{
public sealed class IsExternalInit
{
}
}
El compilador coincidirá con el tipo por nombre completo. No es necesario que aparezca en la biblioteca
principal. Si hay varios tipos por este nombre, el compilador establecerá una interrupción en el orden siguiente:
1. El definido en el proyecto que se está compilando.
2. El que se define en corelib
Si no existe ninguno de ellos, se emitirá un error de ambigüedad de tipos.
El diseño de IsExternalInit es el que se trata en este problema .
Preguntas
Últimos cambios
Uno de los puntos dinámicos principales en la forma en que se codifica esta característica se desconectará a la
siguiente pregunta:
Reemplazar init por set y, por tanto, crear una propiedad totalmente modificable nunca es un cambio
importante en el origen en una propiedad no virtual. Simplemente expande el conjunto de escenarios donde se
puede escribir la propiedad. El único comportamiento en cuestión es si esto sigue siendo un cambio de
interrupción binario.
Si deseamos realizar el cambio de init en set un origen y un cambio compatible binario, se forzará nuestra
mano en la decisión de modreq frente a los atributos a continuación, ya que se descartará modreqs como una
solución. Si, por otro lado, se considera que no es interesante, esto hará que la decisión de modreq frente a los
atributos sea menos afectada.
Solución de LDM no percibe este escenario como convincente.
Modreqs frente a atributos
La estrategia de emisión de init descriptores de acceso de propiedad debe elegir entre usar atributos o
modreqs cuando se emiten durante los metadatos. Estos tienen diferentes desventajas que deben tenerse en
cuenta.
La anotación de un descriptor de acceso set de propiedad con una declaración modreq significa que los
compiladores compatibles con la CLI omitirán el descriptor de acceso a menos que entienda el modreq. Esto
significa que solo los compiladores que reconocen de leerán init el miembro. Los compiladores que
desconocen init omitirán el set descriptor de acceso y, por lo tanto, no tratarán accidentalmente la
propiedad como de lectura/escritura.
El inconveniente de modreq se init convierte en parte de la signatura binaria del set descriptor de acceso. Al
agregar o quitar init se interrumpirá la Compatbility binaria de la aplicación.
El uso de atributos para anotar el set descriptor de acceso significa que solo los compiladores que entienden el
atributo sabrán limitar el acceso a él. Un compilador no consciente de lo init verá como una propiedad de
lectura y escritura simple y permitir el acceso.
Aparentemente, esto significa que esta decisión es una opción entre la seguridad adicional a costa de la
compatibilidad binaria. Profundizar en un poco la seguridad adicional no es exactamente lo que parece. No se
protegerá frente a las siguientes circunstancias:
1. Reflexión sobre public miembros
2. El uso de dynamic
3. Compiladores que no reconocen modreqs
También se debe tener en cuenta que, cuando se completan las reglas de comprobación de IL para .NET 5, init
será una de esas reglas. Esto significa que se obtendrá una aplicación adicional simplemente comprobando que
los compiladores emitan IL comprobable.
Los idiomas primarios para .NET (C#, F # y VB) se actualizarán para que reconozcan estos init descriptores de
acceso. Por lo tanto, el único escenario realista aquí es cuando un compilador de C# 9 emite init propiedades
y se ven mediante un conjunto de herramientas anterior como C# 8, VB 15, etc. C# 8. Esta es la desventaja que
se debe tener en cuenta y sopesar con respecto a la compatibilidad binaria.
Nota: Este debate se aplica principalmente solo a los miembros, no a los campos. Aunque init LDM rechazó
los campos, todavía es interesante tener en cuenta la explicación de modreq frente a los atributos. La init
característica para los campos es una flexibilización de la restricción existente de readonly . Esto significa que, si
se emiten los campos como readonly un atributo, no hay ningún riesgo de que los compiladores anteriores no
utilicen el campo porque ya lo reconocerían readonly . Por lo tanto, el uso de modreq aquí no agrega ninguna
protección adicional.
Solución de La característica usará un modreq para codificar el init establecedor de propiedad. Los factores
atractivos fueron (en ningún orden determinado):
Deseo evitar que los compiladores anteriores infrinjan la init semántica
Deseo de agregar o quitar init en una virtual declaración, o bien interface un cambio de origen y de
interrupción binaria.
Dado que no había ninguna compatibilidad significativa para quitar init para ser un cambio de compatibilidad
binaria, se podía elegir usar modreq straight forward.
init frente a InitOnly
Hay tres formas de sintaxis que tienen una consideración significativa durante nuestra reunión con LDM:
// 1. Use init
int Option1 { get; init; }
// 2. Use init set
int Option2 { get; init set; }
// 3. Use initonly
int Option3 { get; initonly; }
Se tomó la decisión de avanzar con init como un descriptor de acceso independiente en la lista de
descriptores de acceso de propiedad.
Advertir al inicializar un error
Considere el siguiente escenario: Un tipo declara un init único miembro que no se establece en el constructor.
¿El código que construye el objeto obtiene una advertencia si no pudo inicializar el valor?
En ese momento, está claro que el campo nunca se establecerá y, por tanto, tiene muchas similitudes con la
advertencia que indica que no se pueden inicializar los private datos. Por lo tanto, una advertencia parecería
tener algún valor aquí?
Sin embargo, hay importantes desventajas en esta ADVERTENCIA:
1. Complica el caso de compatibilidad de cambiar readonly a init .
2. Requiere incluir metadatos adicionales para indicar los miembros que se deben inicializar mediante el autor
de la llamada.
Además, si creemos que hay un valor aquí en el escenario general de forzar a los creadores de objetos a recibir
advertencias o errores sobre campos específicos, lo más probable es que esto resulte útil como una
característica general. No hay ninguna razón por la que se debe limitar solo a init los miembros.
Solución de No habrá ninguna advertencia sobre el consumo de init campos y propiedades.
LDM quiere obtener una explicación más amplia de la idea de los campos y propiedades necesarios. Esto puede
hacer que volvamos y reconsideremos nuestra posición en init los miembros y la validación.
class Student
{
public init string FirstName;
public init string LastName;
}
En los metadatos, estos campos se marcarían de la misma manera que readonly los campos, pero con un
atributo adicional o modreq para indicar que son init campos de estilo.
Solución de LDM acepta esta propuesta es un sonido pero, en general, el escenario no se ha unido a las
propiedades. La decisión era continuar solo con init propiedades por ahora. Esto tiene un nivel de flexibilidad
adecuado, ya que una init propiedad puede mutar un readonly campo en el tipo declarativo de la propiedad.
Se volverá a considerar si hay comentarios de clientes importantes que justifiquen el escenario.
Permitir init como modificador de tipo
Del mismo modo, el readonly modificador se puede aplicar a struct para declarar automáticamente todos los
campos como readonly , el init único modificador se puede declarar en struct o class para marcar
automáticamente todos los campos como init . Esto significa que las dos declaraciones de tipos siguientes
son equivalentes:
struct Point
{
public init int X;
public init int Y;
}
// vs.
Solución de Esta característica es demasiado bonito aquí y entra en conflicto con la readonly struct
característica en la que se basa. La readonly struct característica es sencilla, ya que se aplica readonly a todos
los miembros: campos, métodos, etc... La init struct característica solo se aplicaría a las propiedades. En
realidad, esto acaba haciéndolo confuso para los usuarios.
Dado que init solo es válido en determinados aspectos de un tipo, hemos rechazado la idea de tenerlo como
un modificador de tipo.
Consideraciones
Compatibilidad
La init característica está diseñada para ser compatible con get las propiedades solo existentes. En concreto,
se trata de un cambio totalmente aditivo para una propiedad que solo es get hoy, pero quiere más semántica
de creación de objetos flexbile.
Por ejemplo, considere el siguiente tipo:
class Name
{
public string First { get; }
public string Last { get; }
class Name
{
public string First { get; init; }
public string Last { get; init; }
Comprobación de IL
Cuando .NET Core decide volver a implementar la comprobación de IL, se deben ajustar las reglas para que
tengan en cuenta init los miembros. Deberá incluirse en los cambios de la regla para las entradas de acceso
no mutadas a los readonly datos.
Las reglas de comprobación de IL deberán dividirse en dos partes:
1. Permitir init a los miembros establecer un readonly campo.
2. Determinar cuándo init se puede llamar legalmente a un miembro.
El primero es un ajuste sencillo de las reglas existentes. El comprobador de IL puede init impartirse para
reconocer miembros y, desde allí, solo tiene que considerar un readonly campo para que se pueda establecer
en this dicho miembro.
La segunda regla es más complicada. En el caso simple de los inicializadores de objeto, la regla es directa. Debe
ser legal llamar a init los miembros cuando el resultado de una new expresión todavía está en la pila. Es decir,
hasta que el valor se haya almacenado en un campo o elemento de matriz local o se haya pasado como un
argumento a otro método, seguirá siendo válido para llamar a init los miembros. Esto garantiza que una vez
que el resultado de la new expresión se publique en un identificador con nombre (distinto de this ), ya no será
válido para llamar a init los miembros.
Sin embargo, el caso más complicado es cuando se mezclan init miembros, inicializadores de objeto y await
. Esto puede hacer que el objeto recién creado se ponga temporalmente en una máquina de Estados y, por tanto,
se coloque en un campo.
Aquí el resultado de será new Student() hoised en una máquina de Estados como un campo antes de que se
produzca el conjunto de Name . El compilador tendrá que marcar los campos activados de forma que el
comprobador de IL comprenda que no son accesibles para el usuario y, por lo tanto, no infringen la semántica
prevista de init .
miembros init
El init modificador se puede extender para aplicarse a todos los miembros de instancia. Esto haría generalizar
el concepto de init durante la construcción de objetos y permitir que los tipos declaren métodos auxiliares
que podrían partipate en el proceso de construcción para inicializar init campos y propiedades.
Estos miembros tendrían todos los restricions que un init descriptor de acceso hace en este diseño. No
obstante, la necesidad es cuestionable y se puede Agregar de forma segura en una versión futura del lenguaje
de una manera compatible.
Generación de tres descriptores de acceso
Una posible implementación de init propiedades es hacer que sea init completamente independiente de
set . Esto significa que una propiedad puede tener tres descriptores de acceso diferentes: get , set y init .
Esto tiene la ventaja potencial de permitir el uso de modreq para aplicar la corrección y mantener la
compatibilidad binaria. La implement