Díganos qué opina sobre la experiencia de descarga del PDF.
Documentación de C#
Aprenda a escribir aplicaciones con el lenguaje de programación C# en la plataforma
.NET.
Aprender a programar en C#
b INTRODUCCIÓN
Aprender C# | Tutoriales, cursos, vídeos y mucho más
q VIDEO
Serie de vídeos para principiantes de C#
Flujo para principiantes de C#
Serie de vídeos para usuarios de nivel intermedio de C#
g TUTORIAL
Tutoriales autoguiados
Tutorial en el explorador
i REFERENCIA
C# en Q&A
Lenguajes en foros de la comunidad tecnológica de .NET
C# en Stack Overflow
C# en Discord
Aspectos básicos de C#
e INFORMACIÓN GENERAL
Un paseo por C#
Dentro de un programa de C#
Serie de vídeos de información destacada de C#
p CONCEPTO
Sistema de tipos
Programación orientada a objetos
Técnicas funcionales
Excepciones
Estilo de codificación
g TUTORIAL
Mostrar línea de comandos
Introducción a las clases
C# orientado a objetos
Conversión de tipos
Detección de patrones
Uso de LINQ para consultar datos
Conceptos clave
e INFORMACIÓN GENERAL
Conceptos de programación
f INICIO RÁPIDO
Métodos
Propiedades
Indizadores
Iterators
Delegados
Events
p CONCEPTO
Tipos de referencia que aceptan valores NULL
Migraciones de referencia que admiten un valor NULL
Resolución de advertencias que admiten un valor NULL
Language-Integrated Query (LINQ)
Control de versiones
Novedades
h NOVEDADES
Novedades de C# 11
Novedades de C# 10
Novedades de C# 9.0
Novedades de C# 8.0
g TUTORIAL
Exploración de los tipos de registros
Exploración de instrucciones de nivel superior
Exploración de nuevos patrones
Actualización de interfaces de forma segura
Creación de mixins con interfaces
Exploración de los índices y rangos
Tipos de referencia que aceptan valores NULL
Exploración de flujos asincrónicos
Escritura de un controlador de interpolación de cadenas personalizado
i REFERENCIA
Cambios importantes en el compilador de C#
Compatibilidad de versiones
Referencia del lenguaje C#
i REFERENCIA
Referencia del lenguaje
g j
Palabras clave de C#
Operadores de C#
Configurar la versión del lenguaje
Especificación del lenguaje C#: borrador de C# 7 en curso
Mantente en contacto
i REFERENCIA
Comunidad de desarrolladores de .NET
YouTube
Twitter
Paseo por el lenguaje C#
Artículo • 15/02/2023 • Tiempo de lectura: 16 minutos
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 el fondo, 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 que
ocupan los objetos que no se utilizan y a los que no se puede acceder. 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 unificado. 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 garantizar que los programas y las bibliotecas
puedan 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 CLR a veces se conoce como "código administrado". El
"código no administrado" se compila en un lenguaje nativo de la máquina destinado a
un sistema concreto.
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 y C++. Hay más de otros 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 amplia variedad de funcionalidades útiles. Las
bibliotecas incluyen 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#:
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. También
puede declarar tipos record , bien sean record struct o record class . Los tipos de
registro tienen miembros sintetizados por el compilador. Los registros se usan
principalmente para almacenar valores, con un comportamiento mínimo asociado.
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 .
C#
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# significa que los tipos de valor se tratan como
referencias de object "a petición". Debido a la unificación, las bibliotecas de uso
general que usan el tipo object se pueden utilizar con todos los tipos que se derivan de
object , incluidos 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
Estructura del programa
Los conceptos organizativos clave de C# son programas, espacios de nombres, tipos,
miembros y ensamblados. Los programas declaran tipos, que contienen miembros y
pueden organizarse en espacios de nombres. Las clases, estructuras e interfaces son
ejemplos de tipos. Los campos, los métodos, las propiedades y los eventos son ejemplos
de miembros. Cuando se compilan programas de C#, se empaquetan físicamente en
ensamblados. Normalmente, los ensamblados tienen las extensiones de archivo .exe o
.dll , en función de si implementan aplicaciones o bibliotecas, respectivamente.
Como ejemplo pequeño, considere la posibilidad de usar un ensamblado que contenga
el código siguiente:
C#
namespace Acme.Collections;
public class Stack<T>
Entry _top;
public void Push(T data)
_top = new Entry(_top, data);
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; }
public Entry(Entry next, T data)
Next = next;
Data = data;
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: una
propiedad llamada Next , otra llamada 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 :
C#
class Example
public static void Main()
var s = new Acme.Collections.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.
Siguiente
Tipos y miembros de C#
Artículo • 10/02/2023 • Tiempo de lectura: 7 minutos
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# proporciona tipos de record class y de record struct 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 :
C#
public class Point
public int X { get; }
public int Y { get; }
public Point(int x, int y) => (X, Y) = (x, y);
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:
C#
var p1 = new Point(0, 0);
var p2 = new Point(10, 20);
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 tipos. 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 :
C#
public class Pair<TFirst, TSecond>
public TFirst First { get; }
public TSecond Second { get; }
public Pair(TFirst first, TSecond second) =>
(First, Second) = (first, second);
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:
C#
var pair = new Pair<int, string>(1, "two");
int i = pair.First; //TFirst int
string s = pair.Second; //TSecond string
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 :
C#
public class Point3D : Point
public int Z { get; set; }
public Point3D(int x, int y, int z) : base(x, y)
Z = z;
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 :
C#
Point a = new(10, 20);
Point b = new Point3D(10, 20, 30);
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 simples, 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.
C#
public struct Point
public double X { get; }
public double Y { get; }
public Point(double x, double y) => (X, Y) = (x, y);
Interfaces
Una interfaz define un contrato que se puede implementar mediante clases y structs.
Una interfaz se define para declarar capacidades que se comparten entre tipos distintos.
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 la herencia múltiple. En el ejemplo siguiente, la interfaz
IComboBox hereda de ITextBox y IListBox .
C#
interface IControl
void Paint();
interface ITextBox : IControl
void SetText(string text);
interface IListBox : IControl
void SetItems(string[] items);
interface IComboBox : ITextBox, IListBox { }
Las clases y los structs pueden implementar varias interfaces. En el ejemplo siguiente, la
clase EditBox implementa IControl y IDataBound .
C#
interface IDataBound
void Bind(Binder b);
public class EditBox : IControl, IDataBound
public void Paint() { }
public 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
C#
EditBox editBox = new();
IControl control = editBox;
IDataBound dataBound = editBox;
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:
C#
public enum SomeRootVegetable
HorseRadish,
Radish,
Turnip
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:
C#
[Flags]
public enum Seasons
None = 0,
Summer = 1,
Autumn = 2,
Winter = 4,
Spring = 8,
All = Summer | Autumn | Winter | Spring
En el ejemplo siguiente se muestran las declaraciones de ambas enumeraciones
anteriores:
C#
var turnip = SomeRootVegetable.Turnip;
var spring = Seasons.Spring;
var startingOnEquinox = Seasons.Spring | Seasons.Autumn;
var theYear = Seasons.All;
Tipos que aceptan valores NULL
Las variables de cualquier tipo se pueden declarar para que no acepten valores NULL o
sí acepten valores NULL. Una variable que acepta valores NULL puede contener un valor
null adicional que no indica valor alguno. Los tipos de valor que aceptan valores NULL
(estructuras o enumeraciones) se representan mediante System.Nullable<T>. Los tipos
de referencia que no aceptan valores NULL y los que sí aceptan valores NULL se
representan mediante el tipo de referencia subyacente. La distinción se representa
mediante metadatos leídos por el compilador y algunas bibliotecas. El compilador
proporciona advertencias cuando se desreferencian las referencias que aceptan valores
NULL sin comprobar primero su valor con null . El compilador también proporciona
advertencias cuando las referencias que no aceptan valores NULL se asignan a un valor
que puede ser null . En el ejemplo siguiente se declara un elemento int que admite un
valor NULL, y que se inicializa en null . A continuación, establece el valor en 5 . Muestra
el mismo concepto con una cadena que acepta valores NULL. Para más información,
consulte Tipos de valor que admiten un valor NULL y Tipos de referencia que aceptan
valores NULL.
C#
int? optionalInt = default;
optionalInt = 5;
string? optionalText = default;
optionalText = "Hello World.";
Tuplas
C# admite tuplas, lo que 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:
C#
(double Sum, int Count) t2 = (4.5, 3);
Console.WriteLine($"Sum of {t2.Count} elements is {t2.Sum}.");
//Output:
//Sum of 3 elements is 4.5.
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.
Anterior Siguiente
Bloques de creación de programas de
C#
Artículo • 18/01/2023 • Tiempo de lectura: 26 minutos
Los tipos descritos en el artículo anterior de esta serie sobre el recorrido por C# se crean
mediante estos bloques de creación:
Miembros, como propiedades, campos, métodos y eventos.
Expresiones
Instrucciones
Miembros
Los miembros de un 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 :
C#
public class Color
public static readonly Color Black = new(0, 0, 0);
public static readonly Color White = new(255, 255, 255);
public static readonly Color Red = new(255, 0, 0);
public static readonly Color Green = new(0, 255, 0);
public static readonly Color Blue = new(0, 0, 255);
public byte R;
public byte G;
public byte B;
public Color(byte r, byte g, byte b)
R = r;
G = g;
B = 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:
C#
public override string ToString() => "This is an object";
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 .
C#
static void Swap(ref int x, ref int y)
int temp = x;
x = y;
y = temp;
public static void SwapExample()
int i = 1, j = 2;
Swap(ref i, ref j);
Console.WriteLine($"{i} {j}"); // "2 1"
}
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 ejemplo siguiente se muestra el uso
de parámetros out .
C#
static void Divide(int x, int y, out int quotient, out int remainder)
quotient = x / y;
remainder = x % y;
public static void OutUsage()
Divide(10, 3, out int quo, out int rem);
Console.WriteLine($"{quo} {rem}"); // "3 1"
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.
C#
public class Console
public static void Write(string fmt, params object[] args) { }
public static void WriteLine(string fmt, params object[] args) { }
// ...
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
C#
int x, y, z;
x = 3;
y = 4;
z = 5;
Console.WriteLine("x={0} y={1} z={2}", x, y, z);
es equivalente a escribir lo siguiente.
C#
int x = 3, y = 4, z = 5;
string s = "x={0} y={1} z={2}";
object[] args = new object[3];
args[0] = x;
args[1] = y;
args[2] = z;
Console.WriteLine(s, args);
Cuerpo del método y variables locales
El cuerpo de un método especifica las instrucciones que se ejecutarán cuando se invoca
el método.
Un cuerpo del método puede declarar variables que son específicas de la invocación del
método. Estas variables se denominan variables locales. Una declaración de variable
local especifica un nombre de tipo, un nombre de variable y, posiblemente, un valor
inicial. En el ejemplo siguiente se declara una variable local i con un valor inicial de
cero y una variable local j sin ningún valor inicial.
C#
class Squares
public static void WriteSquares()
int i = 0;
int j;
while (i < 10)
j = i * i;
Console.WriteLine($"{i} x {i} = {j}");
i++;
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.
C#
class Entity
static int s_nextSerialNo;
int _serialNo;
public Entity()
_serialNo = s_nextSerialNo++;
public int GetSerialNo()
return _serialNo;
public static int GetNextSerialNo()
return s_nextSerialNo;
public static void SetNextSerialNo(int value)
s_nextSerialNo = value;
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 .
C#
Entity.SetNextSerialNo(1000);
Entity e1 = new();
Entity e2 = new();
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 *virtual 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.
C#
public abstract class Expression
public abstract double Evaluate(Dictionary<string, object> vars);
public class Constant : Expression
double _value;
public Constant(double value)
_value = value;
public override double Evaluate(Dictionary<string, object> vars)
return _value;
public class VariableReference : Expression
string _name;
public VariableReference(string name)
_name = name;
public override double Evaluate(Dictionary<string, object> vars)
object value = vars[_name] ?? throw new Exception($"Unknown
variable: {_name}");
return Convert.ToDouble(value);
public class Operation : Expression
Expression _left;
char _op;
Expression _right;
public Operation(Expression left, char op, Expression right)
_left = left;
_op = op;
_right = right;
public override double Evaluate(Dictionary<string, object> vars)
double x = _left.Evaluate(vars);
double y = _right.Evaluate(vars);
switch (_op)
case '+': return x + y;
case '-': return x - y;
case '*': return x * y;
case '/': return x / y;
default: throw new Exception("Unknown operator");
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.
C#
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 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 .
C#
Expression e = new Operation(
new VariableReference("x"),
'*',
new Operation(
new VariableReference("y"),
'+',
new Constant(2)
)
);
Dictionary<string, object> vars = new();
vars["x"] = 3;
vars["y"] = 5;
Console.WriteLine(e.Evaluate(vars)); // "21"
vars["x"] = 1.5;
vars["y"] = 9;
Console.WriteLine(e.Evaluate(vars)); // "16.5"
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.
C#
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), T is
{typeof(T)}");
static void F(double x, double y) => Console.WriteLine("F(double,
double)");
public static void UsageExample()
F(); // Invokes F()
F(1); // Invokes F(int)
F(1.0); // Invokes F(double)
F("abc"); // Invokes F<T>(T), T is System.String
F((double)1); // Invokes F(double)
F((object)1); // Invokes F(object)
F<int>(1); // Invokes F<T>(T), T is System.Int32
F(1, 1); // Invokes 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.
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 describen los métodos, que
son los tipos principales de los miembros de función. En esta sección se describen los
otros tipos de miembros de función admitidos por C#: constructores, propiedades,
indexadores, eventos, operadores y finalizadores.
En el ejemplo siguiente se muestra una clase genérica llamada MyList<T> , que
implementa una lista creciente de objetos. La clase contiene varios ejemplos de los tipos
más comunes de miembros de función.
C#
public class MyList<T>
const int DefaultCapacity = 4;
T[] _items;
int _count;
public MyList(int capacity = DefaultCapacity)
_items = new T[capacity];
public int Count => _count;
public int Capacity
get => _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;
public T this[int index]
get => _items[index];
set
if (!object.Equals(_items[index], value)) {
_items[index] = value;
OnChanged();
public void Add(T item)
if (_count == Capacity) Capacity = _count * 2;
_items[_count] = item;
_count++;
OnChanged();
protected virtual void OnChanged() =>
Changed?.Invoke(this, EventArgs.Empty);
public override bool Equals(object other) =>
Equals(this, other as MyList<T>);
static bool Equals(MyList<T> a, MyList<T> b)
if (Object.ReferenceEquals(a, null)) return
Object.ReferenceEquals(b, null);
if (Object.ReferenceEquals(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;
public event EventHandler Changed;
public static bool operator ==(MyList<T> a, MyList<T> b) =>
Equals(a, b);
public static bool operator !=(MyList<T> a, MyList<T> b) =>
!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 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.
C#
MyList<string> list1 = new();
MyList<string> list2 = new(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:
C#
MyList<string> names = new();
names.Capacity = 100; // Invokes set accessor
int i = names.Count; // Invokes get accessor
int j = names.Capacity; // Invokes get accessor
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:
C#
MyList<string> names = new();
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 MyList<T> clase declara un único miembro de evento denominado Changed , que
indica que se ha agregado un nuevo elemento a la lista o se ha cambiado un elemento
de lista mediante el descriptor de acceso set del indexador. 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> .
C#
class EventExample
static int s_changeCount;
static void ListChanged(object sender, EventArgs e)
s_changeCount++;
public static void Usage()
var names = new MyList<string>();
names.Changed += new EventHandler(ListChanged);
names.Add("Liz");
names.Add("Martha");
names.Add("Beth");
Console.WriteLine(s_changeCount); // "3"
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> .
C#
MyList<int> a = new();
a.Add(1);
a.Add(2);
MyList<int> b = new();
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 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) .
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 .
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.
C# ofrece operadores para realizar operaciones aritméticas, lógicas, de desplazamiento
y bit a bit, además de comparaciones de igualdad y de orden.
Para obtener la lista completa de los operadores de C# ordenados por nivel de
prioridad, vea Operadores de C#.
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
Anterior Siguiente
Áreas de lenguaje principales de C#
Artículo • 18/01/2023 • Tiempo de lectura: 10 minutos
En este artículo se presentan las características principales del lenguaje C#.
Matrices, colecciones y LINQ
C# y .NET proporcionan muchos tipos de colecciones diferentes. Las matrices tienen una
sintaxis definida por el lenguaje. Los tipos de colecciones genéricos se enumeran en el
espacio de nombres System.Collections.Generic. Las colecciones especializadas incluyen
System.Span<T> para acceder a la memoria continua en el marco de pila, así como
System.Memory<T> para acceder a la memoria continua en el montón administrado.
Todas las colecciones, incluidas las matrices, Span<T> y Memory<T>, comparten un
principio unificador para la iteración. Puede usar la interfaz
System.Collections.Generic.IEnumerable<T>. Este principio unificador implica que
cualquiera de los tipos de colecciones se puede usar con consultas LINQ u otros
algoritmos. Los métodos se escriben mediante IEnumerable<T>, y esos algoritmos
funcionan con cualquier colección.
Matrices
Una matriz es una estructura de datos que contiene un número de variables a las que se
accede mediante índices calculados. Las variables contenidas en una matriz,
denominadas también elementos de la matriz, son todas del mismo tipo. Este tipo se
denomina 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 operador new . 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 y se imprime
su contenido.
C#
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[{i}] = {a[i]}");
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.
C#
int[] a1 = new int[10];
int[,] a2 = new int[10, 5];
int[,,] a3 = new int[10, 5, 2];
La matriz a1 contiene 10 elementos, la matriz a2 50 (10 × 5) elementos y la matriz a3
100 (10 × 5 × 2) elementos.
El tipo de elemento de una matriz puede ser cualquiera,
incluido un tipo de matriz. Una matriz con elementos de un tipo de matriz a veces se
conoce como matriz escalonada porque las longitudes de las matrices de elementos no
tienen que ser iguales. En el ejemplo siguiente se asigna una matriz de matrices de int :
C#
int[][] a = new int[3][];
a[0] = new int[10];
a[1] = new int[5];
a[2] = new int[20];
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.
C#
int[] a = new int[] { 1, 2, 3 };
La longitud de la matriz se deduce del número de expresiones entre { y } . La
inicialización de la matriz se puede acortar aún más, de modo que no sea necesario
reformular el tipo de matriz.
C#
int[] a = { 1, 2, 3 };
Los dos ejemplos anteriores son equivalentes al código siguiente:
C#
int[] t = new int[3];
t[0] = 1;
t[1] = 2;
t[2] = 3;
int[] a = t;
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:
C#
foreach (int item in a)
Console.WriteLine(item);
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:
C#
Console.WriteLine($"The low and high temperature on {weatherData.Date:MM-dd-
yyyy}");
Console.WriteLine($" was {weatherData.LowTemp} and
{weatherData.HighTemp}.");
// Output (similar to):
// The low and high temperature on 08-11-2020
// was 5 and 30.
Una cadena interpolada se declara mediante el token $ . La interpolación de cadenas
evalúa las expresiones entre { y } , convierte el resultado en un elemento string y
reemplaza el texto entre corchetes por el resultado de cadena de la expresión. El
elemento : presente en la primera expresión, {weatherData.Date:MM-dd-yyyy} ,
especifica la cadena de formato. En el ejemplo anterior, especifica que la fecha debe
imprimirse en formato "MM-dd-aaaa".
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. También puede inspeccionar los elementos de una lista o matriz. La expresión
switch es la expresión primaria para la coincidencia de patrones.
Delegados y expresiones lambda
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 de otros lenguajes. A
diferencia de los punteros de función, los delegados están orientados a objetos y tienen
seguridad de tipos.
En el siguiente ejemplo se declara y usa un tipo de delegado denominado Function .
C#
delegate double Function(double x);
class Multiplier
double _factor;
public Multiplier(double factor) => _factor = factor;
public double Multiply(double x) => x * _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;
public static void Main()
double[] a = { 0.0, 0.5, 1.0 };
double[] squares = Apply(a, (x) => x * x);
double[] sines = Apply(a, Math.Sin);
Multiplier m = new(2.0);
double[] doubles = Apply(a, m.Multiply);
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 una expresión lambda para crear una función
anónima (como (x) => x * x en el ejemplo anterior), un método estático (como
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 o expresiones
lambda, 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:
C#
double[] doubles = Apply(a, (double x) => x * 2.0);
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:
C#
public async Task<int> RetrieveDocsHomePage()
var client = new HttpClient();
byte[] content = await
client.GetByteArrayAsync("https://docs.microsoft.com/");
Console.WriteLine($"{nameof(RetrieveDocsHomePage)}: Finished
downloading.");
return content.Length;
En este pequeño ejemplo se muestran las características principales de la programación
asincrónica:
La declaración del método incluye el modificador async .
El cuerpo del método espera ( await ) a la devolución del método
GetByteArrayAsync .
El tipo especificado en la instrucción return coincide con el argumento de tipo de
la declaración Task<T> para el método. (Un método que devuelva Task usará
instrucciones return sin ningún argumento).
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.
C#
public class HelpAttribute : Attribute
string _url;
string _topic;
public HelpAttribute(string url) => _url = url;
public string Url => _url;
public string Topic
get => _topic;
set => _topic = value;
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.
C#
[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 que definen 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 .
C#
Type widgetType = typeof(Widget);
object[] widgetClassAttributes =
widgetType.GetCustomAttributes(typeof(HelpAttribute), false);
if (widgetClassAttributes.Length > 0)
HelpAttribute attr = (HelpAttribute)widgetClassAttributes[0];
Console.WriteLine($"Widget class help URL : {attr.Url} - Related topic :
{attr.Topic}");
System.Reflection.MethodInfo displayMethod =
widgetType.GetMethod(nameof(Widget.Display));
object[] displayMethodAttributes =
displayMethod.GetCustomAttributes(typeof(HelpAttribute), false);
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.
Anterior
Introducción a C#
Artículo • 10/02/2023 • Tiempo de lectura: 3 minutos
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.
https://docs.microsoft.com/shows/CSharp-101/What-is-C/player
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 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.
101 ejemplos de LINQ
Este ejemplo requiere la herramienta global dotnet-try . Una vez que instale la
herramienta y clone el repositorio try-samples , puede aprender Language Integrated
Query (LINQ) mediante un conjunto de 101 ejemplos que puede ejecutar de forma
interactiva. Puede descubrir diferentes maneras de consultar, explorar y transformar
secuencias de datos.
Configuración de un entorno local
Artículo • 22/09/2022 • Tiempo de lectura: 2 minutos
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.
Flujo de desarrollo de aplicaciones básico
Las instrucciones de estos tutoriales asumen que está usando la CLI de .NET para crear,
compilar y ejecutar aplicaciones. Usará los comandos siguientes:
dotnet new crea una aplicación. Este comando genera los archivos y los recursos
necesarios para la aplicación. Los tutoriales de introducción a C# usan el tipo de
aplicación console . Cuando conozca los conceptos básicos, puede expandirlo a
otros tipos de aplicaciones.
dotnet build compila el archivo ejecutable.
dotnet run ejecuta el archivo ejecutable.
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.
Se recomienda la plantilla de proyecto Console Application .
Se le ofrecerá la posibilidad de especificar una plataforma de destino. Los
tutoriales siguientes funcionan mejor cuando el destino es .NET 5 o versiones
posteriores.
Compilar>Compilar solución crea el arcivo ejecutable.
Depurar>Iniciar sin depurar ejecuta el archivo ejecutable.
Elección del tutorial
Puede empezar con cualquiera de los siguientes tutoriales:
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.
Uso de números enteros y de punto
flotante en C#
Artículo • 29/09/2022 • Tiempo de lectura: 10 minutos
En este tutorial se explican los tipos numéricos en C#. 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.
Consulte Configuración del entorno local para obtener instrucciones de instalación e
información general sobre el desarrollo de aplicaciones en .NET.
Si no quiere configurar un entorno local, consulte la versión interactiva en el explorador
de este tutorial.
Análisis de las operaciones matemáticas con
enteros
Cree un directorio denominado numbers-quickstart. Conviértalo en el directorio actual y
ejecute el siguiente comando:
CLI de .NET
dotnet new console -n NumbersInCSharp -o .
) Importante
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
.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 implícitas global using
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:
C#
int a = 18;
int b = 6;
int c = a + b;
Console.WriteLine(c);
Ejecute este código escribiendo dotnet run en la ventana de comandos.
Ha visto una de las operaciones matemáticas fundamentales con enteros. El tipo int
representa un entero, que puede ser cero o un número entero positivo o negativo. Use
el símbolo + para la suma. Otros operadores matemáticos comunes con enteros son:
- para resta
* para multiplicación
/ para división
Comience por explorar esas operaciones diferentes. Agregue estas líneas después de la
línea que escribe el valor de c :
C#
// subtraction
c = a - b;
Console.WriteLine(c);
// multiplication
c = a * b;
Console.WriteLine(c);
// division
c = a / b;
Console.WriteLine(c);
Ejecute este código escribiendo dotnet run en la ventana de comandos.
Si quiere, también puede probar a escribir varias operaciones matemáticas en la misma
línea. Pruebe c = a + b - 12 * 17; por ejemplo. Se permite la combinación de
variables y números constantes.
Sugerencia
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:
C#
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);
La línea WorkWithIntegers(); invoca el método. El código siguiente declara el método y
lo define.
Análisis sobre el orden de las operaciones
Convierta en comentario la llamada a WorkingWithIntegers() . De este modo la salida
estará menos saturada a medida que trabaje en esta sección:
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() :
C#
int a = 5;
int b = 4;
int c = 2;
int d = a + b * c;
Console.WriteLine(d);
La salida muestra que la multiplicación se realiza antes que la suma.
Puede forzar la ejecución de un orden diferente de las operaciones si la operación o las
operaciones que realizó primero se incluyen entre paréntesis. Agregue las líneas
siguientes y ejecute de nuevo:
C#
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.
C#
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:
C#
int e = 7;
int f = 4;
int g = 3;
int h = (e + f) / g;
Console.WriteLine(h);
Escriba dotnet run de nuevo para ver los resultados.
Antes de continuar, vamos a tomar todo el código que ha escrito en esta sección y a
colocarlo en un nuevo método. Llame a ese nuevo método OrderPrecedence . El código
debería tener este aspecto:
C#
// 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);
Información sobre los límites y la precisión de
los enteros
En el último ejemplo se ha mostrado que la división de enteros trunca el resultado.
Puede obtener el resto con el operador de módulo, el carácter % . Pruebe el código
siguiente tras la llamada de método a OrderPrecedence() :
C#
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:
C#
int max = int.MaxValue;
int min = int.MinValue;
Console.WriteLine($"The range of integers is {min} to {max}");
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:
C#
int what = max + 3;
Console.WriteLine($"An example of overflow: {what}");
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 .
Operaciones con el tipo double
El tipo numérico double representa números de punto flotante de doble precisión.
Puede que no esté familiarizado con estos términos. Un número de punto flotante
resulta útil para representar números no enteros cuya magnitud puede ser muy grande
o pequeña. La precisión doble es un término relativo que describe el número de dígitos
binarios que se usan para almacenar el valor. Los números de precisión doble tienen el
doble del número de dígitos binarios que la precisión sencilla. En los equipos
modernos, es más habitual usar números con precisión doble que con precisión sencilla.
Los números de precisión sencilla se declaran mediante la palabra clave float .
Comencemos a explorar. Agregue el siguiente código y observe el resultado:
C#
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:
C#
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:
C#
double max = double.MaxValue;
double min = double.MinValue;
Console.WriteLine($"The range of double is {min} to {max}");
Estos valores se imprimen en notación científica. El número a la izquierda de E es la
mantisa. El número a la derecha es el exponente, como una potencia de diez. Al igual
que sucede con los números decimales en las operaciones matemáticas, los tipos
double en C# pueden presentar errores de redondeo. Pruebe este código:
C#
double third = 1.0 / 3.0;
Console.WriteLine(third);
Sabe que la repetición de 0.3 un número finito de veces no es exactamente lo mismo
que 1/3 .
Desafío
Pruebe otros cálculos con números grandes, números pequeños, multiplicaciones y
divisiones con el tipo double . Intente realizar cálculos más complicados. Una vez que
haya invertido un tiempo en ello, tome el código que ha escrito y colóquelo en un
nuevo método. Ponga a ese nuevo método el nombre WorkWithDoubles .
Trabajo con tipos decimales
Hasta el momento ha visto los tipos numéricos básicos en C#: los tipos integer y double.
Pero hay otro tipo más que debe conocer: decimal . El tipo decimal tiene un intervalo
más pequeño, pero mayor precisión que double . Observemos lo siguiente:
C#
decimal min = decimal.MinValue;
decimal max = decimal.MaxValue;
Console.WriteLine($"The range of the decimal type is {min} to {max}");
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:
C#
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 .
7 Nota
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
Instrucciones y bucles de C# if : tutorial
de lógica condicional
Artículo • 28/11/2022 • Tiempo de lectura: 10 minutos
En este tutorial se enseña a escribir código de C# 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#.
Sugerencia
Para pegar un fragmento de código dentro del modo de enfoque , debe usar el
método abreviado de teclado ( Ctrl + v o cmd + v ).
Requisitos previos
En el tutorial se espera que tenga una máquina configurada para el desarrollo local.
Consulte Configuración del entorno local para obtener instrucciones de instalación e
información general sobre el desarrollo de aplicaciones en .NET.
Si prefiere ejecutar el código sin tener que configurar un entorno local, consulte la
versión interactiva en el explorador de este tutorial.
Toma de decisiones con la instrucción if .
Cree un directorio denominado branches-tutorial. Conviértalo en el directorio actual y
ejecute el siguiente comando:
CLI de .NET
dotnet new console -n BranchesAndLoops -o .
) Importante
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
.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 implícitas global using
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:
C#
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 "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:
C#
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.
Sugerencia
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.
Operaciones conjuntas con if y else
Para ejecutar un código distinto en las bifurcaciones true y false, cree una bifurcación
else para que se ejecute cuando la condición sea false. Pruebe una rama else . Agregue
las dos últimas líneas del código siguiente (ya debe tener los cuatro primeros):
C#
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.
) Importante
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:
C#
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");
Sugerencia
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:
C#
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:
C#
if ((a + b + c > 10) || (a == b))
Console.WriteLine("The answer is greater than 10");
Console.WriteLine("Or the first number is equal to the second");
else
Console.WriteLine("The answer is not greater than 10");
Console.WriteLine("And the first number is not equal to the second");
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:
C#
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");
if ((a + b + c > 10) || (a > b))
Console.WriteLine("The answer is greater than 10");
Console.WriteLine("Or the first number is greater than the second");
else
Console.WriteLine("The answer is not greater than 10");
Console.WriteLine("And 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:
C#
//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.
Uso de bucles para repetir las operaciones
En esta sección se usan bucles para repetir las instrucciones. Agregue este código
después de la llamada a ExploreIf :
C#
int counter = 0;
while (counter < 10)
Console.WriteLine($"Hello World! The counter is {counter}");
counter++;
La instrucción while comprueba una condición y ejecuta la instrucción o el bloque de
instrucciones que aparece después de while . Comprueba repetidamente la condición,
ejecutando esas instrucciones 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 .
) Importante
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:
C#
int counter = 0;
do
Console.WriteLine($"Hello World! The counter is {counter}");
counter++;
} while (counter < 10);
Este bucle do y el bucle while anterior generan el mismo resultado.
Operaciones con el bucle for
El bucle for se utiliza con frecuencia en C#. Pruebe este código:
C#
for (int index = 0; index < 10; index++)
Console.WriteLine($"Hello World! The index is {index}");
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.
Bucles anidados creados
Se puede anidar un bucle while , do o for dentro de otro para crear una matriz
mediante la combinación de cada elemento del bucle externo con cada elemento del
bucle interno. Vamos a crear un conjunto de pares alfanuméricos para representar filas y
columnas.
Un bucle for puede generar las filas:
C#
for (int row = 1; row < 11; row++)
Console.WriteLine($"The row is {row}");
Otro bucle puede generar las columnas:
C#
for (char column = 'a'; column < 'k'; column++)
Console.WriteLine($"The column is {column}");
Puede anidar un bucle dentro de otro para formar pares:
C#
for (int row = 1; row < 11; row++)
for (char column = 'a'; column < 'k'; column++)
Console.WriteLine($"The cell is ({row}, {column})");
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() .
Combinación de ramas y bucles
Ahora que ya ha obtenido la información sobre el bucle if y las construcciones de
bucles con el lenguaje C#, trate de escribir código de C# para obtener la suma de todos
los enteros de uno a veinte que se puedan dividir entre tres. Tenga en cuenta las
siguientes sugerencias:
El operador % proporciona el resto de una operación de división.
La instrucción if genera la condición para saber si un número debe formar parte
de la suma.
El bucle for puede facilitar la repetición de una serie de pasos para todos los
números comprendidos entre el uno y el veinte.
Pruébelo usted mismo. Después, revise cómo lo ha hecho. Debe obtener 63 como
respuesta. Puede ver una respuesta posible mediante la visualización del código
completado en GitHub .
Ha completado el tutorial "Bifurcaciones y bucles".
Puede continuar con el tutorial Matrices y colecciones en su propio entorno de
desarrollo.
Puede aprender más sobre estos conceptos en los artículos siguientes:
Instrucciones de selección
Instrucciones de iteración
Aprenda a administrar colecciones de
datos mediante List<T> en C#
Artículo • 07/03/2023 • Tiempo de lectura: 6 minutos
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.
Consulte Configuración del entorno local para obtener instrucciones de instalación e
información general sobre el desarrollo de aplicaciones en .NET.
Si prefiere ejecutar el código sin tener que configurar un entorno local, consulte la
versión interactiva en el explorador de este tutorial.
Un ejemplo de lista básico
Cree un directorio denominado list-tutorial. Conviértalo en el directorio actual y ejecute
dotnet new console .
) Importante
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
.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 implícitas global using
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 código existente por el siguiente:
C#
var names = new List<string> { "<name>", "Ana", "Felipe" };
foreach (var name in names)
Console.WriteLine($"Hello {name.ToUpper()}!");
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.
Modificación del contenido de las listas
La colección creada usa el tipo List<T>. Este tipo almacena las secuencias de elementos.
Especifique el tipo de los elementos entre corchetes angulares.
Un aspecto importante de este tipo List<T> es que se puede aumentar o reducir, lo que
permite agregar o quitar elementos. Agregue este código al final del programa:
C#
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:
C#
Console.WriteLine($"My name is {names[0]}");
Console.WriteLine($"I've added {names[2]} and {names[3]} to the list");
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:
C#
Console.WriteLine($"The list has {names.Count} people in it");
Guarde el archivo y vuelva a escribir dotnet run para ver los resultados.
Búsqueda y orden en las listas
En los ejemplos se usan listas relativamente pequeñas, pero las aplicaciones a menudo
pueden crear listas que contengan muchos más elementos, en ocasiones, con una
numeración que engloba millares. Para encontrar elementos en estas colecciones más
grandes, debe buscar diferentes elementos en la lista. El método IndexOf busca un
elemento y devuelve su índice. Si el elemento no está en la lista, IndexOf devuelve -1 .
Agregue este código a la parte inferior del programa:
C#
var index = names.IndexOf("Felipe");
if (index == -1)
Console.WriteLine($"When an item is not found, IndexOf returns
{index}");
else
Console.WriteLine($"The name {names[index]} is at index {index}");
index = names.IndexOf("Not Found");
if (index == -1)
Console.WriteLine($"When an item is not found, IndexOf returns
{index}");
else
Console.WriteLine($"The name {names[index]} is at index {index}");
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:
C#
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:
C#
WorkWithStrings();
void WorkWithStrings()
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()}!");
Console.WriteLine($"My name is {names[0]}");
Console.WriteLine($"I've added {names[2]} and {names[3]} to the list");
Console.WriteLine($"The list has {names.Count} people in it");
var index = names.IndexOf("Felipe");
if (index == -1)
Console.WriteLine($"When an item is not found, IndexOf returns
{index}");
else
Console.WriteLine($"The name {names[index]} is at index {index}");
index = names.IndexOf("Not Found");
if (index == -1)
Console.WriteLine($"When an item is not found, IndexOf returns
{index}");
else
Console.WriteLine($"The name {names[index]} is at index {index}");
names.Sort();
foreach (var name in names)
Console.WriteLine($"Hello {name.ToUpper()}!");
Listas de otros tipos
Hasta el momento, se ha usado el tipo string en las listas. Se va a crear una lista
List<T> con un tipo distinto. Se va a crear una serie de números.
Agregue lo siguiente al programa después de llamar a WorkWithStrings() :
C#
var fibonacciNumbers = new List<int> {1, 1};
Se crea una lista de enteros y se definen los dos primeros enteros con el valor 1. Son los
dos primeros valores de una sucesión de Fibonacci, una secuencia de números. Cada
número sucesivo de Fibonacci se obtiene con la suma de los dos números anteriores.
Agregue este código:
C#
var previous = fibonacciNumbers[fibonacciNumbers.Count - 1];
var previous2 = fibonacciNumbers[fibonacciNumbers.Count - 2];
fibonacciNumbers.Add(previous + previous2);
foreach (var item in fibonacciNumbers)
Console.WriteLine(item);
Guarde el archivo y escriba dotnet run para ver los resultados.
Sugerencia
Para centrarse solo en esta sección, puede comentar el código que llama a
WorkWithStrings(); . Solo debe colocar dos caracteres / delante de la llamada,
como en: // WorkWithStrings(); .
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#
Artículo • 15/02/2023 • Tiempo de lectura: 2 minutos
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.
C#
// A skeleton of a C# program
using System;
// Your program starts here:
Console.WriteLine("Hello world!");
namespace YourNamespace
class YourClass
struct YourStruct
interface IYourInterface
delegate int YourDelegate();
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:
C#
// A skeleton of a C# program
using System;
namespace YourNamespace
class YourClass
struct YourStruct
interface IYourInterface
delegate int YourDelegate();
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
Especificación del lenguaje C#
Para obtener más información, consulte la sección Conceptos básicos de Especificación
del lenguaje C#. La especificación del lenguaje es la fuente definitiva de la sintaxis y el
uso de C#.
Main() y argumentos de línea de
comandos
Artículo • 15/02/2023 • Tiempo de lectura: 9 minutos
El método Main es el punto de entrada de una aplicación de C# (las bibliotecas y los
servicios no requieren un método Main como punto de entrada). Cuando se inicia la
aplicación, el método Main es el primero que se invoca.
Solo puede haber un punto de entrada en un programa de C#. Si hay más de una clase
que tenga un método Main , deberá compilar el programa con la opción del compilador
StartupObject para especificar qué método Main quiere utilizar como punto de entrada.
Para obtener más información, consulte StartupObject (opciones del compilador de C#).
C#
class TestClass
static void Main(string[] args)
// Display the number of command line arguments.
Console.WriteLine(args.Length);
A partir de C# 9, puede omitir el método Main y escribir instrucciones de C# como si
estuvieran en el método Main , como en el ejemplo siguiente:
C#
using System.Text;
StringBuilder builder = new();
builder.AppendLine("Hello");
builder.AppendLine("World!");
Console.WriteLine(builder.ToString());
Para obtener información sobre cómo escribir código de aplicación con un método de
punto de entrada implícito, consulte las instrucciones de nivel superior.
Información general
El método Main es el punto de entrada de un programa ejecutable; es donde se
inicia y finaliza el control del programa.
Main se declara dentro de una clase o estructura. El valor de Main debe ser static y
no public. (En el ejemplo anterior, recibe el acceso predeterminado de private). La
clase o estructura envolvente no debe ser estático.
Main puede tener un tipo de valor devuelto void , int , Task o Task<int> .
Solo si Main devuelve un tipo de valor devuelto Task o Task<int> , la declaración
de Main puede incluir el modificador async, Esto excluye específicamente un
método async void Main .
El método Main se puede declarar con o sin un parámetro string[] que contiene
los argumentos de línea de comandos. Al usar Visual Studio para crear aplicaciones
Windows, se puede agregar el parámetro manualmente o usar el método
GetCommandLineArgs() con el fin de obtener los argumentos de la línea de
comandos. Los parámetros se leen como argumentos de línea de comandos
indizados con cero. A diferencia de C y C++, el nombre del programa no se trata
como el primer argumento de línea de comandos en la matriz args , pero es el
primer elemento del método GetCommandLineArgs().
En la lista siguiente se muestran las signaturas Main válidas:
C#
public static void Main() { }
public static int Main() { }
public static void Main(string[] args) { }
public static int Main(string[] args) { }
public static async Task Main() { }
public static async Task<int> Main() { }
public static async Task Main(string[] args) { }
public static async Task<int> Main(string[] args) { }
En los ejemplos anteriores se usa el modificador de acceso public . Esto es habitual,
pero no es necesario.
Al agregar los tipos de valor devuelto async , Task y Task<int> , se simplifica el código
de programa cuando las aplicaciones de consola tienen que realizar tareas de inicio y
await de operaciones asincrónicas en Main .
Valores devueltos Main()
Puede devolver un objeto int desde el método Main si lo define de una de las
siguientes maneras:
Código del método Main Signatura de Main
No se usa args ni await static int Main()
Se usa args , no se usa await static int Main(string[] args)
No se usa args , se usa await static async Task<int> Main()
Se usan args y await static async Task<int> Main(string[] args)
Si el valor devuelto de Main no se usa, la devolución de void o Task permite que el
código sea ligeramente más sencillo.
Código del método Main Signatura de Main
No se usa args ni await static void Main()
Se usa args , no se usa await static void Main(string[] args)
No se usa args , se usa await static async Task Main()
Se usan args y await static async Task Main(string[] args)
Pero, si se devuelve int o Task<int> , el programa puede comunicar información de
estado a otros programas o scripts que invocan el archivo ejecutable.
En el ejemplo siguiente se muestra cómo se puede acceder al código de salida para el
proceso.
En este ejemplo se usan las herramientas de línea de comandos de .NET Core. Si no está
familiarizado con las herramientas de línea de comandos de .NET Core, puede obtener
información sobre ellas en este artículo de introducción.
Cree una aplicación mediante la ejecución de dotnet new console . Modifique el método
Main en Program.cs como se indica a continuación:
C#
// Save this program as MainReturnValTest.cs.
class MainReturnValTest
static int Main()
//...
return 0;
Cuando un programa se ejecuta en Windows, cualquier valor devuelto por la función
Main se almacena en una variable de entorno. Esta variable de entorno se puede
recuperar mediante ERRORLEVEL desde un archivo por lotes, o mediante $LastExitCode
desde PowerShell.
Puede compilar la aplicación mediante el comando dotnet build de la CLI de dotnet.
Después, cree un script de PowerShell para ejecutar la aplicación y mostrar el resultado.
Pegue el código siguiente en un archivo de texto y guárdelo como test.ps1 en la
carpeta que contiene el proyecto. Ejecute el script de PowerShell. Para ello, escriba
test.ps1 en el símbolo del sistema de PowerShell.
Dado que el código devuelve el valor cero, el archivo por lotes comunicará un resultado
satisfactorio. En cambio, si cambia MainReturnValTest.cs para que devuelva un valor
distinto de cero y luego vuelve a compilar el programa, la ejecución posterior del script
de PowerShell informará de que se ha producido un error.
PowerShell
dotnet run
if ($LastExitCode -eq 0) {
Write-Host "Execution succeeded"
} else
Write-Host "Execution Failed"
Write-Host "Return value = " $LastExitCode
Resultados
Execution succeeded
Return value = 0
Valores devueltos asincrónicos de Main
Cuando se declara un valor devuelto async para Main , el compilador genera el código
reutilizable para llamar a métodos asincrónicos en Main . Si no especifica la palabra clave
async , debe escribir ese código usted mismo, como se muestra en el siguiente ejemplo.
El código del ejemplo garantiza que el programa se ejecute hasta que se complete la
operación asincrónica:
C#
public static void Main()
AsyncConsoleWork().GetAwaiter().GetResult();
private static async Task<int> AsyncConsoleWork()
// Main body here
return 0;
Este código reutilizable se puede reemplazar por:
C#
static async Task<int> Main(string[] args)
return await AsyncConsoleWork();
Una ventaja de declarar Main como async es que el compilador siempre genera el
código correcto.
Cuando el punto de entrada de la aplicación devuelve Task o Task<int> , el compilador
genera un nuevo punto de entrada que llama al método de punto de entrada declarado
en el código de la aplicación. Suponiendo que este punto de entrada se denomina
$GeneratedMain , el compilador genera el código siguiente para estos puntos de entrada:
static Task Main() hace que el compilador emita el equivalente de private
static void $GeneratedMain() => Main().GetAwaiter().GetResult();
static Task Main(string[]) hace que el compilador emita el equivalente de
private static void $GeneratedMain(string[] args) =>
Main(args).GetAwaiter().GetResult();
static Task<int> Main() hace que el compilador emita el equivalente de private
static int $GeneratedMain() => Main().GetAwaiter().GetResult();
static Task<int> Main(string[]) hace que el compilador emita el equivalente de
private static int $GeneratedMain(string[] args) =>
Main(args).GetAwaiter().GetResult();
7 Nota
Si en los ejemplos se usase el modificador async en el método Main , el compilador
generaría el mismo código.
Argumentos de la línea de comandos
Puede enviar argumentos al método Main definiéndolo de una de las siguientes
maneras:
Código del método Main Signatura de Main
No hay valores devueltos, no se usa await static void Main(string[] args)
Valor devuelto, no se usa await static int Main(string[] args)
No hay valores devueltos, se usa await static async Task Main(string[] args)
Valor devuelto, se usa await static async Task<int> Main(string[] args)
Si no se usan los argumentos, puede omitir args de la signatura del método para
simplificar ligeramente el código:
Código del método Main Signatura de Main
No hay valores devueltos, no se usa await static void Main()
Valor devuelto, no se usa await static int Main()
No hay valores devueltos, se usa await static async Task Main()
Valor devuelto, se usa await static async Task<int> Main()
7 Nota
También puede usar Environment.CommandLine o
Environment.GetCommandLineArgs para acceder a los argumentos de la línea de
comandos desde cualquier punto de una consola o de una aplicación de Windows
Forms. Para habilitar los argumentos de la línea de comandos en la signatura de
método Main de una aplicación de Windows Forms, tendrá que modificar
manualmente la signatura de Main . El código generado por el diseñador de
Windows Forms crea un Main sin ningún parámetro de entrada.
El parámetro del método Main es una matriz String que representa los argumentos de la
línea de comandos. Normalmente, para determinar si hay argumentos, se prueba la
propiedad Length ; por ejemplo:
C#
if (args.Length == 0)
System.Console.WriteLine("Please enter a numeric argument.");
return 1;
Sugerencia
La matriz args no puede ser NULL. así que es seguro acceder a la propiedad
Length sin comprobar los valores NULL.
También puede convertir los argumentos de cadena en tipos numéricos mediante la
clase Convert o el método Parse . Por ejemplo, la siguiente instrucción convierte la
string en un número long mediante el método Parse:
C#
long num = Int64.Parse(args[0]);
También se puede usar el tipo de C# long , que tiene como alias Int64 :
C#
long num = long.Parse(args[0]);
También puede usar el método ToInt64 de la clase Convert para hacer lo mismo:
C#
long num = Convert.ToInt64(s);
Para obtener más información, vea Parse y Convert.
En el ejemplo siguiente se muestra cómo usar argumentos de la línea de comandos en
una aplicación de consola. La aplicación toma un argumento en tiempo de ejecución, lo
convierte en un entero y calcula el factorial del número. Si no se proporciona ningún
argumento, la aplicación emite un mensaje en el que se explica el uso correcto del
programa.
Para compilar y ejecutar la aplicación desde un símbolo del sistema, siga estos pasos:
1. Pegue el código siguiente en cualquier editor de texto, y después guarde el
archivo como archivo de texto con el nombre Factorial.cs.
C#
public class Functions
public static long Factorial(int n)
// Test for invalid input.
if ((n < 0) || (n > 20))
return -1;
// Calculate the factorial iteratively rather than recursively.
long tempResult = 1;
for (int i = 1; i <= n; i++)
tempResult *= i;
return tempResult;
class MainClass
static int Main(string[] args)
// Test if input arguments were supplied.
if (args.Length == 0)
Console.WriteLine("Please enter a numeric argument.");
Console.WriteLine("Usage: Factorial <num>");
return 1;
// Try to convert the input arguments to numbers. This will
throw
// an exception if the argument is not a number.
// num = int.Parse(args[0]);
int num;
bool test = int.TryParse(args[0], out num);
if (!test)
Console.WriteLine("Please enter a numeric argument.");
Console.WriteLine("Usage: Factorial <num>");
return 1;
// Calculate factorial.
long result = Functions.Factorial(num);
// Print result.
if (result == -1)
Console.WriteLine("Input must be >= 0 and <= 20.");
else
Console.WriteLine($"The Factorial of {num} is {result}.");
return 0;
// If 3 is entered on command line, the
// output reads: The factorial of 3 is 6.
2. En la pantalla Inicio o en el menú Inicio, abra una ventana del Símbolo del sistema
para desarrolladores de Visual Studio y navegue hasta la carpeta que contiene el
archivo que creó.
3. Escriba el siguiente comando para compilar la aplicación.
dotnet build
Si la aplicación no tiene ningún error de compilación, se creará un archivo
ejecutable con el nombre Factorial.exe.
4. Escriba el siguiente comando para calcular el factorial de 3:
dotnet run -- 3
5. El comando genera este resultado: The factorial of 3 is 6.
7 Nota
Al ejecutar una aplicación en Visual Studio, puede especificar argumentos de la
línea de comandos en la Página Depuración, Diseñador de proyectos.
Especificación del lenguaje C#
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#.
Vea también
System.Environment
Procedimiento para mostrar argumentos de la línea de comandos
Instrucciones de nivel superior:
programas sin métodos Main
Artículo • 15/02/2023 • Tiempo de lectura: 3 minutos
A partir de C# 9, no es necesario incluir explícitamente un método Main en un proyecto
de aplicación de consola. En su lugar, puede usar la característica de instrucciones de
nivel superior para minimizar el código que tiene que escribir. En este caso, el
compilador genera una clase y un punto de entrada de método Main para la aplicación.
Aquí se muestra un archivo Program.cs que es un programa de C# completo en C# 10:
C#
Console.WriteLine("Hello World!");
Las instrucciones de nivel superior permiten escribir programas sencillos para utilidades
pequeñas, como Azure Functions y Acciones de GitHub. También facilitan a los nuevos
programadores de C# empezar a aprender y escribir código.
En las secciones siguientes se explican las reglas de lo que puede y no puede hacer con
las instrucciones de nivel superior.
Solo un archivo de nivel superior
Una aplicación solo debe tener un punto de entrada. Un proyecto solo puede tener un
archivo con instrucciones de nivel superior. Al colocar instrucciones de nivel superior en
más de un archivo de un proyecto, se produce el error del compilador siguiente:
CS8802 Solo una unidad de compilación puede tener instrucciones de nivel
superior.
Un proyecto puede tener cualquier número de archivos de código fuente adicionales
que no tengan instrucciones de nivel superior.
Ningún otro punto de entrada
Puede escribir un método Main de forma explícita, pero no puede funcionar como
punto de entrada. El compilador emite la advertencia siguiente:
CS7022 El punto de entrada del programa es código global: se ignora el punto de
entrada "Main()".
En un proyecto con instrucciones de nivel superior, no se puede usar la opción del
compilador -main para seleccionar el punto de entrada, incluso si el proyecto tiene uno
o varios métodos Main .
Directivas using
Si incluye directivas using, deben aparecer en primer lugar en el archivo, como en este
ejemplo:
C#
using System.Text;
StringBuilder builder = new();
builder.AppendLine("Hello");
builder.AppendLine("World!");
Console.WriteLine(builder.ToString());
Espacio de nombres global
Las instrucciones de nivel superior están implícitamente en el espacio de nombres
global.
Espacios de nombres y definiciones de tipos
Un archivo con instrucciones de nivel superior también puede contener espacios de
nombres y definiciones de tipos, pero deben aparecer después de las instrucciones de
nivel superior. Por ejemplo:
C#
MyClass.TestMethod();
MyNamespace.MyClass.MyMethod();
public class MyClass
public static void TestMethod()
Console.WriteLine("Hello World!");
namespace MyNamespace
class MyClass
public static void MyMethod()
Console.WriteLine("Hello World from
MyNamespace.MyClass.MyMethod!");
args
Las instrucciones de nivel superior pueden hacer referencia a la variable args para
acceder a los argumentos de línea de comandos que se hayan escrito. La variable args
nunca es NULL, pero su valor Length es cero si no se han proporcionado argumentos de
línea de comandos. Por ejemplo:
C#
if (args.Length > 0)
foreach (var arg in args)
Console.WriteLine($"Argument={arg}");
else
Console.WriteLine("No arguments");
await
Puede llamar a un método asincrónico mediante el uso await . Por ejemplo:
C#
Console.Write("Hello ");
await Task.Delay(5000);
Console.WriteLine("World!");
Código de salida para el proceso
Para devolver un valor int cuando finaliza la aplicación, use la instrucción return como
lo haría en un método Main que devuelva una instancia de int . Por ejemplo:
C#
string? s = Console.ReadLine();
int returnValue = int.Parse(s ?? "-1");
return returnValue;
Método de punto de entrada implícito
El compilador genera un método que actúa como el punto de entrada del programa
para un proyecto con instrucciones de nivel superior. El nombre de este método no es
en realidad Main , es un detalle de implementación al que el código no puede hacer
referencia directamente. La signatura del método depende de si las instrucciones de
nivel superior contienen la palabra clave await o la instrucción return . En la tabla
siguiente se muestra el aspecto que tendría la signatura del método, utilizando el
nombre del método Main en la tabla para mayor comodidad.
El código de nivel superior contiene Signatura de Main implícita
await y return static async Task<int> Main(string[] args)
await static async Task Main(string[] args)
return static int Main(string[] args)
No await ni return static void Main(string[] args)
Especificación del lenguaje C#
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#.
Especificación de la característica: instrucciones de nivel superior
El sistema de tipos de C#
Artículo • 15/02/2023 • Tiempo de lectura: 16 minutos
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, el tipo y naturaleza (valor, referencia o salida) para cada
parámetro de entrada y para el valor devuelto. La biblioteca de clases .NET define tipos
numéricos integrados, así como tipos complejos que representan una amplia variedad
de construcciones. Entre ellas se incluyen 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:
C#
int a = 5;
int b = a + 2; //OK
bool test = true;
// Error. Operator '+' cannot be applied to operands of type 'int' and
'bool'.
int c = a + test;
7 Nota
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.
Especificar tipos en declaraciones de variable
Cuando declare una variable o constante en un programa, debe especificar su tipo o
utilizar la palabra clave var para que el compilador infiera el tipo. En el ejemplo siguiente
se muestran algunas declaraciones de variable que utilizan tanto tipos numéricos
integrados como tipos complejos definidos por el usuario:
C#
// Declaration only:
float temperature;
string name;
MyClass myClass;
// Declaration with initializers (four examples):
char firstLetter = 'C';
var limit = 3;
int[] source = { 0, 1, 2, 3, 4, 5 };
var query = from item in source
where item <= limit
select item;
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:
C#
public string GetName(int ID)
if (ID < names.Length)
return names[ID];
else
return String.Empty;
private string[] names = { "Spencer", "Sally", "Doug" };
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. Estos representan 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 que los define. 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.
Common Type System
Es importante entender dos aspectos fundamentales sobre el sistema de tipos en .NET:
Es compatible con el principio de herencia. Los tipos pueden derivarse de otros
tipos, denominados tipos base. El tipo derivado hereda (con algunas restricciones),
los métodos, las propiedades y otros miembros del tipo base. A su vez, el tipo base
puede derivarse de algún otro tipo, en cuyo caso el tipo derivado hereda los
miembros de ambos tipos base en su jerarquía de herencia. Todos los tipos,
incluidos los tipos numéricos integrados como System.Int32 (palabra clave de C#:
int ), se derivan en última instancia de un único tipo base, que es System.Object
(palabra clave de C#: object). Esta jerarquía de tipos unificada se denomina
Common Type System (CTS). Para más información sobre la herencia en C#, vea
Herencia.
En CTS, cada tipo se define como un tipo de valor o un tipo de referencia. Estos
tipos incluyen todos los tipos personalizados de la biblioteca de clases .NET y
también sus propios tipos definidos por el usuario. Los tipos que se definen
mediante el uso de la palabra clave struct son tipos de valor; todos los tipos
numéricos integrados son structs . Los tipos que se definen mediante el uso de la
palabra clave class o record son tipos de referencia. Los tipos de referencia y los
tipos de valor tienen distintas reglas de tiempo de compilación y distintos
comportamientos de tiempo de ejecución.
En la ilustración siguiente se muestra la relación entre los tipos de valor y los tipos de
referencia en CTS.
7 Nota
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. Los miembros
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 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.
Los tipos de registro pueden ser tipos de referencia ( record class ) o tipos de valor
( record struct ).
En general, las clases se utilizan para modelar comportamientos más complejos. Las
clases suelen almacenar datos que están diseñados para modificarse después de crear
un objeto de clase. Los structs son más adecuados para estructuras de datos pequeñas.
Los structs suelen almacenar datos que no están diseñados para modificarse después de
que se haya creado el struct. Los tipos de registro son estructuras de datos con
miembros sintetizados del compilador adicionales. Los registros suelen almacenar datos
que no están diseñados para modificarse después de que se haya creado 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. La memoria de un struct
se asigna en línea 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. Puede declarar tipos record
struct que son tipos de valor e incluir los miembros sintetizados para los registros.
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:
C#
// constant field on type byte.
byte b = byte.MaxValue;
Pero se declaran y se les asignan valores como si fueran tipos simples no agregados:
C#
byte num = 0xA;
int i = 5;
char c = 'Z';
Los tipos de valor están sellados. 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.
No se puede convertir un tipo de struct en cualquier tipo de interfaz que implemente.
Esta conversión provoca una que operació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:
C#
public struct Coords
public int x, y;
public Coords(int p1, int p2)
x = p1;
y = p2;
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:
C#
public enum FileMode
CreateNew = 1,
Create = 2,
Open = 3,
OpenOrCreate = 4,
Truncate = 5,
Append = 6,
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 reference
type.
Al declarar una variable de un reference type, contiene el valor null hasta que se asigna
con una instancia de ese tipo o se crea una mediante el operador new. La creación y
asignación de una clase se muestran en el ejemplo siguiente:
C#
MyClass myClass = new MyClass();
MyClass myClass2 = myClass;
No se puede crear una instancia de interface directamente mediante el operador new.
En su lugar, cree y asigne una instancia de una clase que implemente la interfaz.
Considere el ejemplo siguiente:
C#
MyClass myClass = new MyClass();
// Declare and assign using an existing value.
IMyInterface myInterface = myClass;
// Or create and assign a value in a single statement.
IMyInterface myInterface2 = new MyClass();
Cuando se crea un objeto, se asigna memoria en el montón administrado. La variable
contiene solo una referencia a la ubicación del objeto. Los tipos del montón
administrado producen sobrecarga cuando se asignan y cuando se reclaman. La
recolección de elementos no utilizados es la funcionalidad de administración automática
de memoria de CLR, que realiza la recuperación. 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. El usuario las declara
y las usa con la sintaxis simplificada que proporciona C#, como se muestra en el
ejemplo siguiente:
C#
// Declare and initialize an array of integers.
int[] nums = { 1, 2, 3, 4, 5 };
// Access an instance property of System.Array.
int len = nums.Length;
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. 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 de valores literales
En C#, los valores literales reciben un tipo del compilador. Puede especificar cómo debe
escribirse un literal numérico; para ello, anexe una letra al final del número. Por ejemplo,
para especificar que el valor 4.56 debe tratarse como un valor float , anexe "f" o "F"
después del número: 4.56f . Si no se anexa ninguna letra, el compilador inferirá un tipo
para el literal. Para obtener más información sobre los tipos que se pueden especificar
con sufijos de letras, vea Tipos numéricos integrales y Tipos numéricos de punto
flotante.
Dado que los literales tienen tipo y todos los tipos derivan en última instancia de
System.Object, puede escribir y compilar código como el siguiente:
C#
string s = "The answer is " + 5.ToString();
// Outputs: "The answer is 5"
Console.WriteLine(s);
Type type = 12345.GetType();
// Outputs: "System.Int32"
Console.WriteLine(type);
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). El código de cliente
proporciona el tipo concreto cuando crea 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 :
C#
List<string> stringList = new List<string>();
stringList.Add("String example");
// compile time error adding a type other than a string:
stringList.Add(4);
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.
Tipos implícitos, tipos anónimos y tipos que
admiten un valor NULL
Como se ha mencionado anteriormente, puede asignar implícitamente un tipo a una
variable local (pero no miembros de clase) mediante la palabra clave var. La variable
sigue recibiendo un tipo en tiempo de compilación, pero este lo proporciona el
compilador. Para más información, vea Variables locales con asignación implícita de
tipos.
En algunos casos, resulta conveniente crear un tipo con nombre para conjuntos sencillos
de valores relacionados que no desea almacenar ni pasar fuera de los límites del
método. Puede crear tipos anónimos para este fin. Para obtener más información,
consulte Tipos anónimos (Guía de programación de C#).
Los tipos de valor normales no pueden tener un valor null, pero se pueden crear tipos de
valor que aceptan valores NULL mediante la adición de ? después del tipo. Por ejemplo,
int? es un tipo int que también puede tener el valor null. Los tipos que admiten un
valor NULL son instancias del tipo struct genérico System.Nullable<T>. Los tipos que
admiten un valor NULL son especialmente útiles cuando hay un intercambio de datos
con bases de datos en las que los valores numéricos podrían ser null . Para más
información, vea Tipos que admiten un valor NULL.
Tipo en tiempo de compilación y tipo en
tiempo de ejecución
Una variable puede tener distintos tipos en tiempo de compilación y en tiempo de
ejecución. El tipo en tiempo de compilación es el tipo declarado o inferido de la variable
en el código fuente. El tipo en tiempo de ejecución es el tipo de la instancia a la que hace
referencia esa variable. A menudo, estos dos tipos son los mismos, como en el ejemplo
siguiente:
C#
string message = "This is a string of characters";
En otros casos, el tipo en tiempo de compilación es diferente, tal y como se muestra en
los dos ejemplos siguientes:
C#
object anotherMessage = "This is another string of characters";
IEnumerable<char> someCharacters = "abcdefghijklmnopqrstuvwxyz";
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
Especificación del lenguaje C#
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#.
Declaración de espacios de nombres
para organizar los tipos
Artículo • 15/02/2023 • Tiempo de lectura: 2 minutos
Los espacios de nombres se usan mucho en programación de C# de dos maneras. En
primer lugar, .NET usa espacios de nombres para organizar sus clases de la siguiente
manera:
C#
System.Console.WriteLine("Hello World!");
System es un espacio de nombres y Console es una clase de ese espacio de nombres. La
palabra clave using se puede usar para que el nombre completo no sea necesario,
como en el ejemplo siguiente:
C#
using System;
C#
Console.WriteLine("Hello World!");
Para más información, vea using (Directiva).
) Importante
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
.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 implícitas global using
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.
En segundo lugar, declarar sus propios espacios de nombres puede ayudarle a controlar
el ámbito de nombres de clase y método en proyectos de programación grandes. Use la
palabra clave namespace para declarar un espacio de nombres, como en el ejemplo
siguiente:
C#
namespace SampleNamespace
class SampleClass
public void SampleMethod()
System.Console.WriteLine(
"SampleMethod inside SampleNamespace");
El nombre del espacio de nombres debe ser un nombre de identificador de C# válido.
A partir de C# 10, puede declarar un espacio de nombres para todos los tipos definidos
en ese archivo, como se muestra en el ejemplo siguiente:
C#
namespace SampleNamespace;
class AnotherSampleClass
public void AnotherSampleMethod()
System.Console.WriteLine(
"SampleMethod inside SampleNamespace");
La ventaja de esta nueva sintaxis es que es más sencilla, lo que ahorra espacio horizontal
y llaves. Esto facilita la lectura del código.
Información general sobre los espacios de
nombres
Los espacios de nombres tienen las propiedades siguientes:
Organizan proyectos de código de gran tamaño.
Se delimitan mediante el operador . .
La directiva using obvia la necesidad de especificar el nombre del espacio de
nombres para cada clase.
El espacio de nombres global es el espacio de nombres "raíz": global::System
siempre hará referencia al espacio de nombres System de .NET.
Especificación del lenguaje C#
Para más información, vea la sección Espacio de nombres de la Especificación del
lenguaje C#.
Introducción a las clases
Artículo • 15/02/2023 • Tiempo de lectura: 5 minutos
Tipos de referencia
Un tipo que se define como una class, 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 expresamente una instancia de la clase mediante el operador new o se le asigne
un objeto de un tipo compatible que se ha creado en otro lugar, tal y como se muestra
en el ejemplo siguiente:
C#
//Declaring an object of type MyClass.
MyClass mc = new MyClass();
//Declaring another object of the same type, assigning it the value of the
first object.
MyClass mc2 = mc;
Cuando se crea el objeto, se asigna suficiente memoria en el montón administrado para
ese objeto específico y la variable solo contiene una referencia a la ubicación de dicho
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 automática de la memoria y
recolección de elementos no utilizados.
Declarar clases
Las clases se declaran mediante la palabra clave class seguida por un identificador
único, como se muestra en el siguiente ejemplo:
C#
//[access modifier] - [class] - [identifier]
public class Customer
// Fields, properties, methods and events go here...
La palabra clave class va precedida del nivel de acceso. Como en este caso se usa
public, cualquier usuario puede crear instancias de esta clase. El nombre de la clase
sigue a la palabra clave class . El nombre de la clase debe ser un nombre de
identificador de C# válido. El resto de la definición es el cuerpo de la clase, donde se
definen los datos y el comportamiento. Los campos, las propiedades, los métodos y los
eventos de una clase se denominan de manera colectiva miembros de clase.
Creación de objetos
Aunque a veces se usan indistintamente, una clase y un objeto son cosas diferentes. Una
clase define un tipo de objeto, pero no es un objeto en sí. Un objeto es una entidad
concreta basada en una clase y, a veces, se conoce como una instancia de una clase.
Los objetos se pueden crear usando la palabra clave new , seguida del nombre de la
clase en la que se basará el objeto, como en este ejemplo:
C#
Customer object1 = new Customer();
Cuando se crea una instancia de una clase, se vuelve a pasar al programador una
referencia al objeto. En el ejemplo anterior, object1 es una referencia a un objeto que
se basa en Customer . Esta referencia apunta al objeto nuevo, pero no contiene los datos
del objeto. De hecho, puede crear una referencia de objeto sin tener que crear ningún
objeto:
C#
Customer object2;
No se recomienda crear referencias de objeto como la anterior, que no hace referencia a
ningún objeto, ya que, si se intenta obtener acceso a un objeto a través de este tipo de
referencia, se producirá un error en tiempo de ejecución. Pero este tipo de referencia se
puede haber creado para hacer referencia a un objeto, ya sea creando uno o
asignándola a un objeto existente, como en el ejemplo siguiente:
C#
Customer object3 = new Customer();
Customer object4 = object3;
Este código crea dos referencias de objeto que hacen referencia al mismo objeto. Por lo
tanto, los cambios efectuados en el objeto mediante object3 se reflejan en los usos
posteriores de object4 . Dado que los objetos basados en clases se tratan por referencia,
las clases se denominan "tipos de referencia".
Herencia de clases
Las clases admiten completamente la herencia, una característica fundamental de la
programación orientada a objetos. Al crear una clase, puede heredar de cualquier otra
que no esté definida como sealed, y otras clases pueden heredar de esa e invalidar sus
métodos virtuales. Además, puede implementar una o varias interfaces.
La herencia se consigue mediante una derivación, en la que se declara una clase
mediante una clase base, desde la que hereda los datos y el comportamiento. Una clase
base se especifica anexando dos puntos y el nombre de la clase base seguido del
nombre de la clase derivada, como en el siguiente ejemplo:
C#
public class Manager : Employee
// Employee fields, properties, methods and events are inherited
// New Manager fields, properties, methods and events go here...
Cuando una clase declara una clase base, hereda todos los miembros de la clase base
excepto los constructores. Para obtener más información, vea Herencia.
Una clase de C# solo puede heredar directamente de una clase base. En cambio, dado
que una clase base puede heredar de otra clase, una clase podría heredar
indirectamente varias clases base. Además, una clase puede implementar directamente
una o varias interfaces. Para obtener más información, vea Interfaces.
Una clase puede declararse abstract. Una clase abstracta contiene métodos abstractos
que tienen una definición de firma, pero no tienen ninguna implementación. No se
pueden crear instancias de las clases abstractas. Solo se pueden usar a través de las
clases derivadas que implementan los métodos abstractos. Por el contrario, la clase
sealed no permite que otras clases se deriven de ella. Para más información, vea Clases y
miembros de clase abstractos y sellados.
Las definiciones de clase se pueden dividir entre distintos archivos de código fuente.
Para más información, vea Clases y métodos parciales.
Ejemplo
En el ejemplo siguiente se define una clase pública que contiene una propiedad
implementada automáticamente, un método y un método especial denominado
constructor. Para obtener más información, consulte los artículos Propiedades, Métodos
y Constructores. Luego, se crea una instancia de las instancias de la clase con la palabra
clave new .
C#
using System;
public class Person
// Constructor that takes no arguments:
public Person()
Name = "unknown";
// Constructor that takes one argument:
public Person(string name)
Name = name;
// Auto-implemented readonly property:
public string Name { get; }
// Method that overrides the base class (System.Object) implementation.
public override string ToString()
return Name;
class TestPerson
static void Main()
// Call the constructor that has no parameters.
var person1 = new Person();
Console.WriteLine(person1.Name);
// Call the constructor that has one parameter.
var person2 = new Person("Sarah Jones");
Console.WriteLine(person2.Name);
// Get the string representation of the person2 instance.
Console.WriteLine(person2);
// Output:
// unknown
// Sarah Jones
// Sarah Jones
Especificación del lenguaje C#
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#.
Introducción a los tipos de registro en C
#
Artículo • 15/02/2023 • Tiempo de lectura: 4 minutos
Un registro en C# es una clase o estructura que proporciona sintaxis y comportamiento
especiales para trabajar con modelos de datos.
Cuándo se usan los registros
Considere la posibilidad de usar un registro en lugar de una clase o estructura en los
escenarios siguientes:
Desea definir un modelo de datos que dependa de la igualdad de valores.
Desea definir un tipo para el que los objetos son inmutables.
Igualdad de valores
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. Para otros tipos de referencia, como las clases, la igualdad significa igualdad
de referencias. Es decir, dos variables de un tipo de clase son iguales si hacen referencia
al mismo objeto. Los métodos y operadores que determinan la igualdad de dos
instancias de registro usan la igualdad de valores.
No todos los modelos de datos funcionan bien con la igualdad de valores. Por ejemplo,
Entity Framework Core depende de la igualdad de referencias 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.
Inmutabilidad
Un tipo inmutable es aquél que impide cambiar cualquier valor de propiedad o campo
de un objeto una vez creada su instancia. La inmutabilidad puede ser útil cuando se
necesita que un tipo sea seguro para los subprocesos o si depende de que un código
hash quede igual en una tabla hash. Los registros proporcionan una sintaxis concisa
para crear y trabajar con tipos inmutables.
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.
Diferencias entre los registros y las clases y
estructuras
La misma sintaxis que declara y crea instancias de clases o estructuras se puede usar con
los registros. Basta con sustituir la palabra clave class por record , o bien usar record
struct en lugar de struct . Del mismo modo, las clases de registro admiten la misma
sintaxis para expresar las relaciones de herencia. Los registros se diferencian de las
clases de las siguientes maneras:
Puede usar parámetros posicionales para crear un tipo y sus instancias con
propiedades inmutables.
Los mismos métodos y operadores que indican la igualdad o desigualdad de la
referencia en las clases (como Object.Equals(Object) y == ), indican la igualdad o
desigualdad de valores en los registros.
Puede usar una expresión with para crear una copia de un objeto inmutable con
nuevos valores en las propiedades seleccionadas.
El método ToString de un registro crea una cadena con formato que muestra el
nombre de tipo de un objeto y los nombres y valores de todas sus propiedades
públicas.
Un registro puede heredar de otro registro. Un registro no puede heredar de una
clase y una clase no puede heredar de un registro.
Las estructuras de registro se diferencian de las estructuras en que el compilador
sintetiza los métodos para la igualdad y ToString . El compilador sintetiza un método
Deconstruct para las estructuras de registro posicional.
Ejemplos
En el ejemplo siguiente se define un registro público que usa parámetros posicionales
para declarar y crear instancias de un registro. A continuación, imprime el nombre de
tipo y los valores de propiedad:
C#
public record Person(string FirstName, string LastName);
public static void Main()
Person person = new("Nancy", "Davolio");
Console.WriteLine(person);
// output: Person { FirstName = Nancy, LastName = Davolio }
En el ejemplo siguiente se muestra la igualdad de valores en los registros:
C#
public record Person(string FirstName, string LastName, string[]
PhoneNumbers);
public static void Main()
var phoneNumbers = new string[2];
Person person1 = new("Nancy", "Davolio", phoneNumbers);
Person person2 = new("Nancy", "Davolio", phoneNumbers);
Console.WriteLine(person1 == person2); // output: True
person1.PhoneNumbers[0] = "555-1234";
Console.WriteLine(person1 == person2); // output: True
Console.WriteLine(ReferenceEquals(person1, person2)); // output: False
En el ejemplo siguiente se muestra el uso de una expresión with para copiar un objeto
inmutable y cambiar una de las propiedades:
C#
public record Person(string FirstName, string LastName)
public string[] PhoneNumbers { get; init; }
public static void Main()
Person person1 = new("Nancy", "Davolio") { PhoneNumbers = new string[1]
};
Console.WriteLine(person1);
// output: Person { FirstName = Nancy, LastName = Davolio, PhoneNumbers
= System.String[] }
Person person2 = person1 with { FirstName = "John" };
Console.WriteLine(person2);
// output: Person { FirstName = John, LastName = Davolio, PhoneNumbers =
System.String[] }
Console.WriteLine(person1 == person2); // output: False
person2 = person1 with { PhoneNumbers = new string[1] };
Console.WriteLine(person2);
// output: Person { FirstName = Nancy, LastName = Davolio, PhoneNumbers
= System.String[] }
Console.WriteLine(person1 == person2); // output: False
person2 = person1 with { };
Console.WriteLine(person1 == person2); // output: True
Para obtener más información, consulte Registros (Referencia de C#).
Especificación del lenguaje C#
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#.
Interfaces: definir el comportamiento de
varios tipos
Artículo • 15/02/2023 • Tiempo de lectura: 5 minutos
Una interfaz contiene las definiciones de un grupo de funcionalidades relacionadas que
una class o una struct no abstracta deben implementar. Una interfaz puede definir
métodos static , que deben tener una implementación. Una interfaz puede definir una
implementación predeterminada de miembros. Una interfaz no puede declarar datos de
instancia, como campos, propiedades implementadas automáticamente o eventos
similares a las propiedades.
Mediante las interfaces puede incluir, por ejemplo, un comportamiento de varios
orígenes en una clase. Esta capacidad es importante en C# porque el lenguaje no
admite la herencia múltiple de clases. Además, debe usar una interfaz si desea simular la
herencia de estructuras, porque no pueden heredar de otra estructura o clase.
Para definir una interfaz, deberá usar la palabra clave interface, tal y como se muestra en
el ejemplo siguiente.
C#
interface IEquatable<T>
bool Equals(T obj);
El nombre de una interfaz debe ser un nombre de identificador de C# válido. Por
convención, los nombres de interfaz comienzan con una letra I mayúscula.
Cualquier clase o estructura que implementa la interfaz IEquatable<T> debe contener
una definición para un método Equals que coincida con la firma que la interfaz
especifica. Como resultado, puede contar con una clase que implementa IEquatable<T>
para contener un método Equals con el que una instancia de la clase puede determinar
si es igual a otra instancia de la misma clase.
La definición de IEquatable<T> no facilita ninguna implementación para Equals . Una
clase o estructura puede implementar varias interfaces, pero una clase solo puede
heredar de una sola clase.
Para obtener más información sobre las clases abstractas, vea Clases y miembros de
clase abstractos y sellados (Guía de programación de C#).
Las interfaces pueden contener propiedades, eventos, indizadores o métodos de
instancia, o bien cualquier combinación de estos cuatro tipos de miembros. Las
interfaces pueden contener constructores estáticos, campos, constantes u operadores. A
partir de C# 11, los miembros de interfaz que no son campos pueden ser static
abstract . Una interfaz no puede contener campos de instancia, constructores de
instancias ni finalizadores. Los miembros de interfaz son públicos de forma
predeterminada y se pueden especificar explícitamente modificadores de accesibilidad,
como public , protected , internal , private , protected internal o private protected .
Un miembro private debe tener una implementación predeterminada.
Para implementar un miembro de interfaz, el miembro correspondiente de la clase de
implementación debe ser público, no estático y tener el mismo nombre y firma que el
miembro de interfaz.
7 Nota
Cuando una interfaz declara miembros estáticos, un tipo que implementa esa
interfaz también puede declarar miembros estáticos con la misma firma. Los
miembros son distintos y se identifican de forma única mediante el tipo que
declara el miembro. El miembro estático declarado en un tipo no reemplaza el
miembro estático que se declara en la interfaz.
Una clase o estructura que implementa una interfaz debe proporcionar una
implementación para todos los miembros declarados sin una implementación
predeterminada proporcionada por la interfaz. Sin embargo, si una clase base
implementa una interfaz, cualquier clase que se derive de la clase base hereda esta
implementación.
En el siguiente ejemplo se muestra una implementación de la interfaz IEquatable<T>. La
clase de implementación Car debe proporcionar una implementación del método
Equals.
C#
public class Car : IEquatable<Car>
public string? Make { get; set; }
public string? Model { get; set; }
public string? Year { get; set; }
// Implementation of IEquatable<T> interface
public bool Equals(Car? car)
return (this.Make, this.Model, this.Year) ==
(car?.Make, car?.Model, car?.Year);
Las propiedades y los indizadores de una clase pueden definir descriptores de acceso
adicionales para una propiedad o indizador que estén definidos en una interfaz. Por
ejemplo, una interfaz puede declarar una propiedad que tenga un descriptor de acceso
get. La clase que implementa la interfaz puede declarar la misma propiedad con un
descriptor de acceso get y set. Sin embargo, si la propiedad o el indizador usan una
implementación explícita, los descriptores de acceso deben coincidir. Para obtener más
información sobre la implementación explícita, vea Implementación de interfaz explícita
y Propiedades de interfaces.
Las interfaces pueden heredar de una o varias interfaces. La interfaz derivada hereda los
miembros de sus interfaces base. Una clase que implementa una interfaz derivada debe
implementar todos los miembros de esta, incluidos los de las interfaces base. Esa clase
puede convertirse implícitamente en la interfaz derivada o en cualquiera de sus
interfaces base. Una clase puede incluir una interfaz varias veces mediante las clases
base que hereda o mediante las interfaces que otras interfaces heredan. Sin embargo, la
clase puede proporcionar una implementación de una interfaz solo una vez y solo si la
clase declara la interfaz como parte de la definición de la clase ( class ClassName :
InterfaceName ). Si la interfaz se hereda porque se heredó una clase base que
implementa la interfaz, la clase base proporciona la implementación de los miembros de
la interfaz. Sin embargo, la clase derivada puede volver a implementar cualquier
miembro de la interfaz virtual, en lugar de usar la implementación heredada. Cuando las
interfaces declaran una implementación predeterminada de un método, cualquier clase
que las implemente heredará la implementación correspondiente (deberá convertir la
instancia de clase al tipo de interfaz para acceder a la implementación predeterminada
en el miembro de la interfaz).
Una clase base también puede implementar miembros de interfaz mediante el uso de
los miembros virtuales. En ese caso, una clase derivada puede cambiar el
comportamiento de la interfaz reemplazando los miembros virtuales. Para obtener más
información sobre los miembros virtuales, vea Polimorfismo.
Resumen de interfaces
Una interfaz tiene las propiedades siguientes:
En las versiones de C# anteriores a la 8.0, una interfaz es como una clase base
abstracta con solo miembros abstractos. Cualquier clase o estructura que
implemente la interfaz debe implementar todos sus miembros.
A partir de la versión 8.0 de C#, una interfaz puede definir implementaciones
predeterminadas para algunos o todos sus miembros. Una clase o estructura que
implemente la interfaz no tiene que implementar los miembros que tengan
implementaciones predeterminadas. Para obtener más información, vea Métodos
de interfaz predeterminados.
No se puede crear una instancia de una interfaz directamente. Sus miembros se
implementan por medio de cualquier clase o estructura que implementa la
interfaz.
Una clase o estructura puede implementar varias interfaces. Una clase puede
heredar una clase base y también implementar una o varias interfaces.
Clases y métodos genéricos
Artículo • 10/01/2023 • Tiempo de lectura: 3 minutos
Los genéricos introducen el concepto de parámetros de tipo a .NET, lo que le permite
diseñar clases y métodos que aplazan la especificación de uno o varios tipos hasta que
el código de cliente declare y cree una instancia de la clase o el método. Por ejemplo, al
usar un parámetro de tipo genérico T , puede escribir una clase única que otro código
de cliente puede usar sin incurrir en el costo o riesgo de conversiones en tiempo de
ejecución u operaciones de conversión boxing, como se muestra aquí:
C#
// Declare the generic class.
public class GenericList<T>
public void Add(T input) { }
class TestGenericList
private class ExampleClass { }
static void Main()
// Declare a list of type int.
GenericList<int> list1 = new GenericList<int>();
list1.Add(1);
// Declare a list of type string.
GenericList<string> list2 = new GenericList<string>();
list2.Add("");
// Declare a list of type ExampleClass.
GenericList<ExampleClass> list3 = new GenericList<ExampleClass>();
list3.Add(new ExampleClass());
Las clases y métodos genéricos combinan reusabilidad, seguridad de tipos y eficacia de
una manera en que sus homólogos no genéricos no pueden. Los genéricos se usan
frecuentemente con colecciones y los métodos que funcionan en ellas. El espacio de
nombres System.Collections.Generic contiene varias clases de colecciones basadas en
genéricos. No se recomiendan las colecciones no genéricas, como ArrayList, y se
mantienen por compatibilidad. Para más información, vea Elementos genéricos en .NET.
También se pueden crear tipos y métodos genéricos personalizados para proporcionar
soluciones y patrones de diseño generalizados propios con seguridad de tipos y
eficaces. En el ejemplo de código siguiente se muestra una clase genérica simple de lista
vinculada para fines de demostración. (En la mayoría de los casos, debe usar la clase
List<T> proporcionada por .NET en lugar de crear la suya propia). El parámetro de tipo
T se usa en diversas ubicaciones donde normalmente se usaría un tipo concreto para
indicar el tipo del elemento almacenado en la lista. Se usa de estas formas:
Como el tipo de un parámetro de método en el método AddHead .
Como el tipo de valor devuelto de la propiedad Data en la clase anidada Node .
Como el tipo de miembro privado data de la clase anidada.
T está disponible para la clase Node anidada. Cuando se crea una instancia de
GenericList<T> con un tipo concreto, por ejemplo como un GenericList<int> , cada
repetición de T se sustituye por int .
C#
// type parameter T in angle brackets
public class GenericList<T>
// The nested class is also generic on T.
private class Node
// T used in non-generic constructor.
public Node(T t)
next = null;
data = t;
private Node? next;
public Node? Next
get { return next; }
set { next = value; }
// T as private member data type.
private T data;
// T as return type of property.
public T Data
get { return data; }
set { data = value; }
private Node? head;
// constructor
public GenericList()
head = null;
// T as method parameter type:
public void AddHead(T t)
Node n = new Node(t);
n.Next = head;
head = n;
public IEnumerator<T> GetEnumerator()
Node? current = head;
while (current != null)
yield return current.Data;
current = current.Next;
En el ejemplo de código siguiente se muestra cómo el código de cliente usa la clase
genérica GenericList<T> para crear una lista de enteros. Simplemente cambiando el
argumento de tipo, el código siguiente puede modificarse fácilmente para crear listas de
cadenas o cualquier otro tipo personalizado:
C#
class TestGenericList
static void Main()
// int is the type argument
GenericList<int> list = new GenericList<int>();
for (int x = 0; x < 10; x++)
list.AddHead(x);
foreach (int i in list)
System.Console.Write(i + " ");
System.Console.WriteLine("\nDone");
Introducción a los genéricos
Use tipos genéricos para maximizar la reutilización del código, la seguridad de
tipos y el rendimiento.
El uso más común de los genéricos es crear clases de colección.
La biblioteca de clases de .NET contiene varias clases de colección genéricas en el
espacio de nombres System.Collections.Generic. Las colecciones genéricas se
deberían usar siempre que sea posible en lugar de clases como ArrayList en el
espacio de nombres System.Collections.
Puede crear sus propias interfaces, clases, métodos, eventos y delegados
genéricos.
Puede limitar las clases genéricas para habilitar el acceso a métodos en tipos de
datos determinados.
Puede obtener información sobre los tipos que se usan en un tipo de datos
genérico en tiempo de ejecución mediante la reflexión.
Especificación del lenguaje C#
Para obtener más información, consulte la Especificación del lenguaje C#.
Vea también
System.Collections.Generic
Elementos genéricos en .NET
Tipos anónimos
Artículo • 03/03/2023 • Tiempo de lectura: 5 minutos
Los tipos anónimos son una manera cómoda de encapsular un conjunto de propiedades
de solo lectura en un único objeto sin tener que definir primero un tipo explícitamente.
El compilador genera el nombre del tipo y no está disponible en el nivel de código
fuente. El compilador deduce el tipo de cada propiedad.
Para crear tipos anónimos, use el operador new con un inicializador de objeto. Para
obtener más información sobre los inicializadores de objeto, vea Inicializadores de
objeto y colección (Guía de programación de C#).
En el ejemplo siguiente se muestra un tipo anónimo que se inicializa con dos
propiedades llamadas Amount y Message .
C#
var v = new { Amount = 108, Message = "Hello" };
// Rest the mouse pointer over v.Amount and v.Message in the following
// statement to verify that their inferred types are int and string.
Console.WriteLine(v.Amount + v.Message);
Los tipos anónimos suelen usarse en la cláusula select de una expresión de consulta
para devolver un subconjunto de las propiedades de cada objeto en la secuencia de
origen. Para más información sobre las consultas, vea LINQ en C#.
Los tipos anónimos contienen una o varias propiedades públicas de solo lectura. No es
válido ningún otro tipo de miembros de clase, como métodos o eventos. La expresión
que se usa para inicializar una propiedad no puede ser null , una función anónima o un
tipo de puntero.
El escenario más habitual es inicializar un tipo anónimo con propiedades de otro tipo.
En el siguiente ejemplo, se da por hecho que existe una clase con el nombre Product . La
clase Product incluye las propiedades Color y Price , junto con otras propiedades que
no son de su interés. La variable products es una colección de objetos Product . La
declaración de tipo anónimo comienza con la palabra clave new . La declaración inicializa
un nuevo tipo que solo usa dos propiedades de Product . El uso de tipos anónimos hace
que la consulta devuelva una cantidad de datos menor.
Si no especifica nombres de miembros en el tipo anónimo, el compilador da a los
miembros de este tipo el nombre de la propiedad que se usa para inicializarlos. Debe
proporcionar un nombre para una propiedad que se está inicializando con una
expresión, como se muestra en el ejemplo anterior. En el siguiente ejemplo, los nombres
de las propiedades del tipo anónimo son Color y Price .
C#
var productQuery =
from prod in products
select new { prod.Color, prod.Price };
foreach (var v in productQuery)
Console.WriteLine("Color={0}, Price={1}", v.Color, v.Price);
Sugerencia
Puede usar la regla de estilo de .NET IDE0037 para aplicar si se prefieren los
nombres de miembros inferidos o explícitos.
También es posible definir un campo por objeto de otro tipo: clase, estructura o incluso
otro tipo anónimo. Se realiza mediante el uso de la variable que contiene este objeto
igual que en el ejemplo siguiente, donde se crean dos tipos anónimos mediante tipos
definidos por el usuario para los que ya se han creado instancias. En ambos casos, el
campo product del tipo anónimo shipment y shipmentWithBonus será de tipo Product
que contiene los valores predeterminados de cada campo. Y el campo bonus será de
tipo anónimo creado por el compilador.
C#
var product = new Product();
var bonus = new { note = "You won!" };
var shipment = new { address = "Nowhere St.", product };
var shipmentWithBonus = new { address = "Somewhere St.", product, bonus };
Normalmente, cuando se usa un tipo anónimo para inicializar una variable, la variable se
declara como variable local con tipo implícito mediante var. El nombre del tipo no se
puede especificar en la declaración de la variable porque solo el compilador tiene
acceso al nombre subyacente del tipo anónimo. Para obtener más información sobre
var , vea Variables locales con asignación implícita de tipos.
Puede crear una matriz de elementos con tipo anónimo combinando una variable local
con tipo implícito y una matriz con tipo implícito, como se muestra en el ejemplo
siguiente.
C#
var anonArray = new[] { new { name = "apple", diam = 4 }, new { name =
"grape", diam = 1 }};
Los tipos anónimos son tipos class que derivan directamente de object y que no se
pueden convertir a ningún tipo excepto object. El compilador proporciona un nombre
para cada tipo anónimo, aunque la aplicación no pueda acceder a él. Desde el punto de
vista de Common Language Runtime, un tipo anónimo no es diferente de otros tipos de
referencia.
Si dos o más inicializadores de objeto anónimo en un ensamblado especifican una
secuencia de propiedades que están en el mismo orden y que tienen los mismos
nombres y tipos, el compilador trata el objeto como instancias del mismo tipo.
Comparten la misma información de tipo generada por el compilador.
Los tipos anónimos admiten la mutación no destructiva en forma de con expresiones.
Esto le permite crear una nueva instancia de un tipo anónimo en el que una o varias
propiedades tienen nuevos valores:
C#
var apple = new { Item = "apples", Price = 1.35 };
var onSale = apple with { Price = 0.79 };
Console.WriteLine(apple);
Console.WriteLine(onSale);
No se puede declarar que un campo, una propiedad, un evento o el tipo de valor
devuelto de un método tengan un tipo anónimo. De forma similar, no se puede declarar
que un parámetro formal de un método, propiedad, constructor o indizador tenga un
tipo anónimo. Para pasar un tipo anónimo, o una colección que contiene tipos
anónimos, como un argumento a un método, puede declarar el parámetro como
object de tipo. Sin embargo, el uso de object para tipos anónimos anula el propósito
de la coincidencia segura. Si tiene que almacenar resultados de consulta o pasarlos
fuera del límite del método, considere la posibilidad de usar un struct o una clase con
nombre normal en lugar de un tipo anónimo.
Como los métodos Equals y GetHashCode de tipos anónimos se definen en términos de
los métodos Equals y GetHashCode de las propiedades, dos instancias del mismo tipo
anónimo son iguales solo si todas sus propiedades son iguales.
Los tipos anónimos invalidan el método ToString, concatenando el nombre y la salida
ToString de cada propiedad rodeada de llaves.
var v = new { Title = "Hello", Age = 24 };
Console.WriteLine(v.ToString()); // "{ Title = Hello, Age = 24 }"
Información general de clases,
estructuras y registros en C#
Artículo • 15/02/2023 • Tiempo de lectura: 5 minutos
En C#, la definición de un tipo (una clase, estructura o registro) es como un plano
técnico que especifica lo que el tipo puede hacer. Un objeto es básicamente un bloque
de memoria que se ha asignado y configurado de acuerdo con el plano. En este artículo
se proporciona información general de estos planos técnicos y sus características. En el
siguiente artículo de esta serie se presentan los objetos.
Encapsulación
A veces se hace referencia a la encapsulación como el primer pilar o principio de la
programación orientada a objetos. 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 más información, consulte el tutorial Programación
orientada a objetos.
Miembros
Los miembros de un tipo incluyen todos los métodos, campos, constantes, propiedades
y eventos. 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 cuando usa 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
Para más información, consulte Miembros.
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 (denominada clase base) contiene automáticamente todos los
miembros públicos, protegidos e internos de la clase base, salvo sus constructores y
finalizadores.
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.
Para más información, vea Herencia y Polimorfismo.
Interfaces
Las clases, las estructuras y los registros pueden implementar varias interfaces.
Implementar 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. Para obtener
más información, vea Clases estáticas y sus miembros.
Tipos anidados
Una clase, estructura o registro se puede anidar dentro de otra clase, estructura o
registro. Para obtener más información, consulte Tipos anidados.
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. Para más información, vea Clases y
métodos parciales.
Inicializadores de objeto
Puede crear instancias e inicializar objetos de clase o estructura, así como colecciones
de objetos, asignando valores a sus propiedades. Para más información, consulte
Procedimiento para inicializar un objeto mediante un inicializador de objeto.
Tipos anónimos
En situaciones donde no es conveniente o necesario crear una clase con nombre, utilice
los tipos anónimos. Los tipos anónimos se definen mediante sus miembros de datos con
nombre. Para obtener más información, consulte Tipos anónimos (Guía de
programación de C#).
Métodos de extensión
Puede "extender" una clase sin crear una clase derivada mediante la creación de un tipo
independiente. Ese tipo contiene métodos a los que se puede llamar como si
perteneciesen al tipo original. Para más información, consulte Métodos de extensión.
Variables locales con asignación implícita de
tipos
Dentro de un método de clase o estructura, puede utilizar tipos implícitos para indicar al
compilador que determine el tipo de una variable en tiempo de compilación. Para más
información, consulte var (referencia de C#).
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. C# 10 presenta el tipo de valor record struct . Un
registro ( record class o record struct ) 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 tienen el
mismo tipo y si, por cada campo, los valores en ambos registros son iguales. Las
clases 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 jerarquías de herencia en clases de registro.
Las clases de
registro admiten la herencia. Las clases de structs no admiten la herencia.
Para obtener más información, consulte Registros.
Especificación del lenguaje C#
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#.
Objetos: creación de instancias de tipos
Artículo • 15/02/2023 • Tiempo de lectura: 5 minutos
Una definición de clase o estructura es como un plano que especifica qué puede hacer
el tipo. Un objeto es básicamente un bloque de memoria que se ha asignado y
configurado de acuerdo con el plano. Un programa puede crear muchos objetos de la
misma clase. Los objetos también se denominan instancias y pueden almacenarse en
una variable con nombre, o en una matriz o colección. El código de cliente es el código
que usa estas variables para llamar a los métodos y acceder a las propiedades públicas
del objeto. En un lenguaje orientado a objetos, como C#, un programa típico consta de
varios objetos que interactúan dinámicamente.
7 Nota
Los tipos estáticos se comportan de forma diferente a lo que se describe aquí. Para
más información, vea Clases estáticas y sus miembros.
Instancias de estructura frente a Instancias de
clase
Puesto que las clases son tipos de referencia, una variable de un objeto de clase
contiene una referencia a la dirección del objeto del montón administrado. Si se asigna
una segunda variable del mismo tipo a la primera variable, ambas variables hacen
referencia al objeto de esa dirección. Este punto se analiza con más detalle más adelante
en este artículo.
Las instancias de clases se crean mediante el operador new. En el ejemplo siguiente,
Person es el tipo, y person1 y person2 son instancias u objetos de ese tipo.
C#
public class Person
public string Name { get; set; }
public int Age { get; set; }
public Person(string name, int age)
Name = name;
Age = age;
// Other properties, methods, events...
class Program
static void Main()
Person person1 = new Person("Leopold", 6);
Console.WriteLine("person1 Name = {0} Age = {1}", person1.Name,
person1.Age);
// Declare new person, assign person1 to it.
Person person2 = person1;
// Change the name of person2, and person1 also changes.
person2.Name = "Molly";
person2.Age = 16;
Console.WriteLine("person2 Name = {0} Age = {1}", person2.Name,
person2.Age);
Console.WriteLine("person1 Name = {0} Age = {1}", person1.Name,
person1.Age);
/*
Output:
person1 Name = Leopold Age = 6
person2 Name = Molly Age = 16
person1 Name = Molly Age = 16
*/
Dado que las estructuras son tipos de valor, una variable de un objeto de estructura
contiene una copia de todo el objeto. También se pueden crear instancias de estructuras
usando el operador new , pero esto no resulta necesario, como se muestra en el ejemplo
siguiente:
C#
namespace Example;
public struct Person
public string Name;
public int Age;
public Person(string name, int age)
Name = name;
Age = age;
public class Application
static void Main()
// Create struct instance and initialize by using "new".
// Memory is allocated on thread stack.
Person p1 = new Person("Alex", 9);
Console.WriteLine("p1 Name = {0} Age = {1}", p1.Name, p1.Age);
// Create new struct object. Note that struct can be initialized
// without using "new".
Person p2 = p1;
// Assign values to p2 members.
p2.Name = "Spencer";
p2.Age = 7;
Console.WriteLine("p2 Name = {0} Age = {1}", p2.Name, p2.Age);
// p1 values remain unchanged because p2 is copy.
Console.WriteLine("p1 Name = {0} Age = {1}", p1.Name, p1.Age);
/*
Output:
p1 Name = Alex Age = 9
p2 Name = Spencer Age = 7
p1 Name = Alex Age = 9
*/
La memoria para p1 y p2 se asigna en la pila de subprocesos. Esta memoria se reclama
junto con el tipo o método en el que se declara. Este es uno de los motivos por los que
se copian las estructuras en la asignación. Por el contrario, la memoria que se asigna a
una instancia de clase la reclama automáticamente (recolección de elementos no
utilizados) Common Language Runtime cuando todas las referencias al objeto se han
salido del ámbito. No es posible destruir de forma determinante un objeto de clase
como en C++. Para obtener más información sobre la recolección de elementos no
utilizados en .NET, vea Recolección de elementos no utilizados.
7 Nota
La asignación y desasignación de memoria en el montón administrado están muy
optimizadas en Common Language Runtime. En la mayoría de los casos, no existe
ninguna diferencia significativa en el costo de rendimiento entre asignar una
instancia de clase en el montón y asignar una instancia de estructura en la pila.
Identidad de objeto frente a igualdad de
valores
Cuando se comparan dos objetos para comprobar si son iguales, primero debe
determinar si quiere saber si las dos variables representan el mismo objeto en la
memoria o si los valores de uno o varios de sus campos son equivalentes. Si tiene
previsto comparar valores, debe tener en cuenta si los objetos son instancias de tipos de
valor (estructuras) o tipos de referencia (clases, delegados y matrices).
Para determinar si dos instancias de clase hacen referencia a la misma ubicación en
la memoria (lo que significa que tienen la misma identidad), use el método estático
Object.Equals. (System.Object es la clase base implícita para todos los tipos de
valor y tipos de referencia, incluidas las clases y estructuras definidas por el
usuario).
Para determinar si los campos de instancia de dos instancias de estructura
presentan los mismos valores, use el método ValueType.Equals. Dado que todas las
estructuras heredan implícitamente de System.ValueType, se llama al método
directamente en el objeto, como se muestra en el ejemplo siguiente:
C#
// Person is defined in the previous example.
//public struct Person
//{
// public string Name;
// public int Age;
// public Person(string name, int age)
// {
// Name = name;
// Age = age;
// }
//}
Person p1 = new Person("Wallace", 75);
Person p2 = new Person("", 42);
p2.Name = "Wallace";
p2.Age = 75;
if (p2.Equals(p1))
Console.WriteLine("p2 and p1 have the same values.");
// Output: p2 and p1 have the same values.
En algunos casos, la implementación de System.ValueType de Equals utiliza la
conversión boxing y la reflexión. Para obtener información sobre cómo
proporcionar un algoritmo de igualdad eficaz que sea específico del tipo, consulte
Definición de la igualdad de valores para un tipo. Los registros son tipos de
referencia que usan semántica de valores para la igualdad.
Para determinar si los valores de los campos de dos instancias de clase son iguales,
puede usar el método Equals o el operador ==. En cambio, úselos solo si la clase
los ha invalidado o sobrecargado para proporcionar una definición personalizada
de lo que significa "igualdad" para los objetos de ese tipo. La clase también puede
implementar la interfaz IEquatable<T> o la interfaz IEqualityComparer<T>. Ambas
interfaces proporcionan métodos que pueden servir para comprobar la igualdad
de valores. Al diseñar sus propias clases que invaliden Equals , asegúrese de seguir
las instrucciones descritas en Procedimiento: Definición de la igualdad de valores
para un tipo y Object.Equals(Object).
Secciones relacionadas
Para obtener más información:
Clases
Constructores
Finalizadores
Eventos
object
Herencia
class
Tipos de estructura
new (operador)
Sistema de tipos comunes
Herencia: deriva tipos para crear un
comportamiento más especializado
Artículo • 15/02/2023 • Tiempo de lectura: 7 minutos
La herencia, junto con la encapsulación y el polimorfismo, es una de las tres
características principales de la programación orientada a objetos. La herencia permite
crear clases que reutilizan, extienden y modifican el comportamiento definido en otras
clases. La clase cuyos miembros se heredan se denomina clase base y la clase que
hereda esos miembros se denomina clase derivada. Una clase derivada solo puede tener
una clase base directa, pero la herencia es transitiva. Si ClassC se deriva de ClassB y
ClassB se deriva de ClassA , ClassC hereda los miembros declarados en ClassB y
ClassA .
7 Nota
Los structs no admiten la herencia, pero pueden implementar interfaces.
Conceptualmente, una clase derivada es una especialización de la clase base. Por
ejemplo, si tiene una clase base Animal , podría tener una clase derivada denominada
Mammal y otra clase derivada denominada Reptile . Mammal es Animal y Reptile también
es Animal , pero cada clase derivada representa especializaciones diferentes de la clase
base.
Las declaraciones de interfaz pueden definir una implementación predeterminada de
sus miembros. Estas implementaciones las heredan las interfaces derivadas y las clases
que implementan esas interfaces. Para más información sobre los métodos de interfaz
predeterminados, consulte el artículo sobre interfaces.
Cuando se define una clase para que derive de otra clase, la clase derivada obtiene
implícitamente todos los miembros de la clase base, salvo sus constructores y sus
finalizadores. La clase derivada reutiliza el código de la clase base sin tener que volver a
implementarlo. Puede agregar más miembros en la clase derivada. La clase derivada
amplía la funcionalidad de la clase base.
En la ilustración siguiente se muestra una clase WorkItem que representa un elemento
de trabajo de un proceso empresarial. Como con todas las clases, se deriva de
System.Object y hereda todos sus métodos. WorkItem agrega seis miembros propios.
Estos miembros incluyen un constructor, porque los constructores no se heredan. La
clase ChangeRequest hereda de WorkItem y representa un tipo concreto de elemento de
trabajo. ChangeRequest agrega dos miembros más a los miembros que hereda de
WorkItem y de Object. Debe agregar su propio constructor y además agrega
originalItemID . La propiedad originalItemID permite que la instancia ChangeRequest
se asocie con el WorkItem original al que se aplica la solicitud de cambio.
En el ejemplo siguiente se muestra cómo se expresan en C# las relaciones de clase de la
ilustración anterior. En el ejemplo también se muestra cómo WorkItem reemplaza el
método virtual Object.ToString y cómo la clase ChangeRequest hereda la implementación
WorkItem del método. En el primer bloque se definen las clases:
C#
// WorkItem implicitly inherits from the Object class.
public class WorkItem
// Static field currentID stores the job ID of the last WorkItem that
// has been created.
private static int currentID;
//Properties.
protected int ID { get; set; }
protected string Title { get; set; }
protected string Description { get; set; }
protected TimeSpan jobLength { get; set; }
// Default constructor. If a derived class does not invoke a base-
// class constructor explicitly, the default constructor is called
// implicitly.
public WorkItem()
ID = 0;
Title = "Default title";
Description = "Default description.";
jobLength = new TimeSpan();
// Instance constructor that has three parameters.
public WorkItem(string title, string desc, TimeSpan joblen)
this.ID = GetNextID();
this.Title = title;
this.Description = desc;
this.jobLength = joblen;
// Static constructor to initialize the static member, currentID. This
// constructor is called one time, automatically, before any instance
// of WorkItem or ChangeRequest is created, or currentID is referenced.
static WorkItem() => currentID = 0;
// currentID is a static field. It is incremented each time a new
// instance of WorkItem is created.
protected int GetNextID() => ++currentID;
// Method Update enables you to update the title and job length of an
// existing WorkItem object.
public void Update(string title, TimeSpan joblen)
this.Title = title;
this.jobLength = joblen;
// Virtual method override of the ToString method that is inherited
// from System.Object.
public override string ToString() =>
$"{this.ID} - {this.Title}";
// ChangeRequest derives from WorkItem and adds a property (originalItemID)
// and two constructors.
public class ChangeRequest : WorkItem
protected int originalItemID { get; set; }
// Constructors. Because neither constructor calls a base-class
// constructor explicitly, the default constructor in the base class
// is called implicitly. The base class must contain a default
// constructor.
// Default constructor for the derived class.
public ChangeRequest() { }
// Instance constructor that has four parameters.
public ChangeRequest(string title, string desc, TimeSpan jobLen,
int originalID)
// The following properties and the GetNexID method are inherited
// from WorkItem.
this.ID = GetNextID();
this.Title = title;
this.Description = desc;
this.jobLength = jobLen;
// Property originalItemID is a member of ChangeRequest, but not
// of WorkItem.
this.originalItemID = originalID;
En el bloque siguiente se muestra cómo usar las clases base y derivadas:
C#
// Create an instance of WorkItem by using the constructor in the
// base class that takes three arguments.
WorkItem item = new WorkItem("Fix Bugs",
"Fix all bugs in my code branch",
new TimeSpan(3, 4, 0, 0));
// Create an instance of ChangeRequest by using the constructor in
// the derived class that takes four arguments.
ChangeRequest change = new ChangeRequest("Change Base Class Design",
"Add members to the class",
new TimeSpan(4, 0, 0),
1);
// Use the ToString method defined in WorkItem.
Console.WriteLine(item.ToString());
// Use the inherited Update method to change the title of the
// ChangeRequest object.
change.Update("Change the Design of the Base Class",
new TimeSpan(4, 0, 0));
// ChangeRequest inherits WorkItem's override of ToString.
Console.WriteLine(change.ToString());
/* Output:
1 - Fix Bugs
2 - Change the Design of the Base Class
*/
Métodos abstractos y virtuales
Cuando una clase base declara un método como virtual, una clase derivada puede
aplicar override al método con una implementación propia. Si una clase base declara un
miembro como abstract, ese método se debe reemplazar en todas las clases no
abstractas que hereden directamente de dicha clase. Si una clase derivada es abstracta,
hereda los miembros abstractos sin implementarlos. Los miembros abstractos y virtuales
son la base del polimorfismo, que es la segunda característica principal de la
programación orientada a objetos. Para obtener más información, vea Polimorfismo .
Clases base abstractas
Puede declarar una clase como abstracta si quiere impedir la creación directa de
instancias mediante el operador new. Una clase abstracta solo se puede usar si a partir
de ella se deriva una clase nueva. Una clase abstracta puede contener una o más firmas
de método que, a su vez, se declaran como abstractas. Estas firmas especifican los
parámetros y el valor devuelto, pero no tienen ninguna implementación (cuerpo del
método). Una clase abstracta no tiene que contener miembros abstractos, pero si lo
hace, la clase se debe declarar como abstracta. Las clases derivadas que no son
abstractas deben proporcionar la implementación para todos los métodos abstractos de
una clase base abstracta.
Interfaces
Una interfaz es un tipo de referencia que define un conjunto de miembros. Todas las
clases y estructuras que implementan esa interfaz deben implementar ese conjunto de
miembros. Una interfaz puede definir una implementación predeterminada para todos o
ninguno de estos miembros. Una clase puede implementar varias interfaces, aunque
solo puede derivar de una única clase base directa.
Las interfaces se usan para definir funciones específicas para clases que no tienen
necesariamente una relación "es un/una". Por ejemplo, la interfaz System.IEquatable<T>
se puede implementar mediante cualquier clase o estructura para determinar si dos
objetos del tipo son equivalentes (pero el tipo define la equivalencia). IEquatable<T> no
implica el mismo tipo de relación "es un/una" que existe entre una clase base y una
clase derivada (por ejemplo, Mammal es Animal ). Para más información, vea Interfaces.
Impedir la derivación adicional
Una clase puede impedir que otras clases hereden de ella, o de cualquiera de sus
miembros, si se declara a sí misma o al miembro como sealed.
Clase derivada que oculta miembros de clase
base
Una clase derivada puede ocultar miembros de clase base si declara los miembros con
el mismo nombre y firma. Se puede usar el modificador new para indicar de forma
explícita que el miembro no está diseñado para reemplazar al miembro base. No es
necesario usar new, pero se generará una advertencia del compilador si no se usa new.
Para obtener más información, vea Control de versiones con las palabras clave Override
y New y Saber cuándo usar las palabras clave Override y New.
Polimorfismo
Artículo • 31/01/2023 • Tiempo de lectura: 8 minutos
El polimorfismo suele considerarse el tercer pilar de la programación orientada a
objetos, después de la encapsulación y la herencia. Polimorfismo es una palabra griega
que significa "con muchas formas" y tiene dos aspectos diferentes:
En tiempo de ejecución, los objetos de una clase derivada pueden ser tratados
como objetos de una clase base en lugares como parámetros de métodos y
colecciones o matrices. Cuando se produce este polimorfismo, el tipo declarado
del objeto ya no es idéntico a su tipo en tiempo de ejecución.
Las clases base pueden definir e implementar métodosvirtuales, y las clases
derivadas pueden invalidarlos, lo que significa que pueden proporcionar su propia
definición e implementación. En tiempo de ejecución, cuando el código de cliente
llama al método, CLR busca el tipo en tiempo de ejecución del objeto e invoca esa
invalidación del método virtual. En el código fuente puede llamar a un método en
una clase base y hacer que se ejecute una versión del método de la clase derivada.
Los métodos virtuales permiten trabajar con grupos de objetos relacionados de manera
uniforme. Por ejemplo, supongamos que tiene una aplicación de dibujo que permite a
un usuario crear varios tipos de formas en una superficie de dibujo. En tiempo de
compilación, no sabe qué tipos de formas en concreto creará el usuario. Sin embargo, la
aplicación tiene que realizar el seguimiento de los distintos tipos de formas que se
crean, y tiene que actualizarlos en respuesta a las acciones del mouse del usuario. Para
solucionar este problema en dos pasos básicos, puede usar el polimorfismo:
1. Crear una jerarquía de clases en la que cada clase de forma específica deriva de
una clase base común.
2. Usar un método virtual para invocar el método apropiado en una clase derivada
mediante una sola llamada al método de la clase base.
Primero, cree una clase base llamada Shape y clases derivadas como Rectangle , Circle
y Triangle . Dé a la clase Shape un método virtual llamado Draw e invalídelo en cada
clase derivada para dibujar la forma determinada que la clase representa. Cree un
objeto List<Shape> y agréguele una instancia de Circle , Triangle y Rectangle .
C#
public class Shape
// A few example members
public int X { get; private set; }
public int Y { get; private set; }
public int Height { get; set; }
public int Width { get; set; }
// Virtual method
public virtual void Draw()
Console.WriteLine("Performing base class drawing tasks");
public class Circle : Shape
public override void Draw()
// Code to draw a circle...
Console.WriteLine("Drawing a circle");
base.Draw();
public class Rectangle : Shape
public override void Draw()
// Code to draw a rectangle...
Console.WriteLine("Drawing a rectangle");
base.Draw();
public class Triangle : Shape
public override void Draw()
// Code to draw a triangle...
Console.WriteLine("Drawing a triangle");
base.Draw();
Para actualizar la superficie de dibujo, use un bucle foreach para iterar por la lista y
llamar al método Draw en cada objeto Shape de la lista. Aunque cada objeto de la lista
tenga un tipo declarado de Shape , se invocará el tipo en tiempo de ejecución (la versión
invalidada del método en cada clase derivada).
C#
// Polymorphism at work #1: a Rectangle, Triangle and Circle
// can all be used wherever a Shape is expected. No cast is
// required because an implicit conversion exists from a derived
// class to its base class.
var shapes = new List<Shape>
new Rectangle(),
new Triangle(),
new Circle()
};
// Polymorphism at work #2: the virtual method Draw is
// invoked on each of the derived classes, not the base class.
foreach (var shape in shapes)
shape.Draw();
/* Output:
Drawing a rectangle
Performing base class drawing tasks
Drawing a triangle
Performing base class drawing tasks
Drawing a circle
Performing base class drawing tasks
*/
En C#, cada tipo es polimórfico porque todos los tipos, incluidos los definidos por el
usuario, heredan de Object.
Introducción al polimorfismo
Miembros virtuales
Cuando una clase derivada hereda de una clase base, incluye todos los miembros de la
clase base. Todo el comportamiento declarado en la clase base forma parte de la clase
derivada. Esto permite que los objetos de la clase derivada se traten como objetos de la
clase base. Los modificadores de acceso ( public , protected , private etc.) determinan si
esos miembros son accesibles desde la implementación de la clase derivada. Los
métodos virtuales proporcionan al diseñador diferentes opciones para el
comportamiento de la clase derivada:
La clase derivada puede invalidar los miembros virtuales de la clase base, y definir
un comportamiento nuevo.
La clase derivada hereda el método de clase base más cercano sin invalidarlo para
conservar el comportamiento existente, pero permite que más clases derivadas
invaliden el método.
La clase derivada puede definir una nueva implementación no virtual de esos
miembros que oculte las implementaciones de la clase base.
Una clase derivada puede invalidar un miembro de la clase base si este se declara como
virtual o abstracto. El miembro derivado debe usar la palabra clave override para indicar
explícitamente que el propósito del método es participar en una invocación virtual. El
siguiente fragmento de código muestra un ejemplo:
C#
public class BaseClass
public virtual void DoWork() { }
public virtual int WorkProperty
get { return 0; }
public class DerivedClass : BaseClass
public override void DoWork() { }
public override int WorkProperty
get { return 0; }
Los campos no pueden ser virtuales; solo pueden serlo los métodos, propiedades,
eventos e indizadores. Cuando una clase derivada invalida un miembro virtual, se llama
a ese miembro aunque se acceda a una instancia de esa clase como una instancia de la
clase base. El siguiente fragmento de código muestra un ejemplo:
C#
DerivedClass B = new DerivedClass();
B.DoWork(); // Calls the new method.
BaseClass A = B;
A.DoWork(); // Also calls the new method.
Los métodos y propiedades virtuales permiten a las clases derivadas extender una clase
base sin necesidad de usar la implementación de clase base de un método. Para obtener
más información, consulte Control de versiones con las palabras clave Override y New.
Una interfaz proporciona otra manera de definir un método o conjunto de métodos
cuya implementación se deja a las clases derivadas.
Ocultación de miembros de clase base con miembros
nuevos
Si quiere que la clase derivada tenga un miembro con el mismo nombre que el de un
miembro de una clase base, puede usar la palabra clave new para ocultar el miembro de
clase base. La palabra clave new se coloca antes que el tipo devuelto del miembro de la
clase que se está reemplazando. El siguiente fragmento de código muestra un ejemplo:
C#
public class BaseClass
public void DoWork() { WorkField++; }
public int WorkField;
public int WorkProperty
get { return 0; }
public class DerivedClass : BaseClass
public new void DoWork() { WorkField++; }
public new int WorkField;
public new int WorkProperty
get { return 0; }
Se puede acceder a los miembros de la clase base ocultos desde el código de cliente si
se convierte la instancia de la clase derivada en una instancia de la clase base. Por
ejemplo:
C#
DerivedClass B = new DerivedClass();
B.DoWork(); // Calls the new method.
BaseClass A = (BaseClass)B;
A.DoWork(); // Calls the old method.
Evasión de que las clases derivadas invaliden los
miembros virtuales
Los miembros virtuales siguen siendo virtuales con independencia de cuántas clases se
hayan declarado entre el miembro virtual y la clase que originalmente lo haya
declarado. Si la clase A declara un miembro virtual y la clase B deriva de A , y la clase C
de B , la clase C hereda el miembro virtual y puede invalidarlo, independientemente de
que la clase B haya declarado una invalidación para ese miembro. El siguiente
fragmento de código muestra un ejemplo:
C#
public class A
public virtual void DoWork() { }
}
public class B : A
public override void DoWork() { }
Una clase derivada puede detener la herencia virtual al declarar una invalidación como
sealed. Para detener la herencia, es necesario colocar la palabra clave sealed antes de la
palabra clave override en la declaración del miembro de la clase. El siguiente
fragmento de código muestra un ejemplo:
C#
public class C : B
public sealed override void DoWork() { }
En el ejemplo anterior, el método DoWork ya no es virtual para ninguna clase que se
derive de C . Sigue siendo virtual para las instancias de C , aunque se conviertan al tipo
B o al tipo A . Los métodos sellados se pueden reemplazar por clases derivadas
mediante la palabra clave new , como se muestra en el ejemplo siguiente:
C#
public class D : C
public new void DoWork() { }
En este caso, si se llama a DoWork en D con una variable de tipo D , se llama a la nueva
instancia de DoWork . Si se usa una variable de tipo C , B o A para acceder a una
instancia de D , la llamada a DoWork seguirá las reglas de herencia virtual y enrutará esas
llamadas a la implementación de DoWork en la clase C .
Acceso a miembros virtuales de clases base desde clases
derivadas
Una clase derivada que ha reemplazado o invalidado un método o propiedad puede
seguir accediendo al método o propiedad en la clase base usando la siguiente palabra
clave base . El siguiente fragmento de código muestra un ejemplo:
C#
public class Base
public virtual void DoWork() {/*...*/ }
public class Derived : Base
public override void DoWork()
//Perform Derived's work here
//...
// Call DoWork on base class
base.DoWork();
Para obtener más información, vea base.
7 Nota
Se recomienda que las máquinas virtuales usen base para llamar a la
implementación de la clase base de ese miembro en su propia implementación.
Dejar que se produzca el comportamiento de la clase base permite a la clase
derivada concentrarse en implementar el comportamiento específico de la clase
derivada. Si no se llama a la implementación de la clase base, depende de la clase
derivada hacer que su comportamiento sea compatible con el de la clase base.
Información general sobre la
coincidencia de patrones
Artículo • 15/02/2023 • Tiempo de lectura: 9 minutos
La coincidencia de patrones es una técnica consistente en probar una expresión para
determinar si tiene ciertas características. La coincidencia de patrones de C#
proporciona una sintaxis más concisa para probar las expresiones y realizar acciones
cuando una expresión coincide. La "expresión is" admite la coincidencia de patrones
para probar una expresión y declarar condicionalmente una nueva variable como el
resultado de esa expresión. La expresión switch permite realizar acciones basadas en el
primer patrón que coincida para una expresión. Estas dos expresiones admiten un
variado vocabulario de patrones.
En este artículo encontrará información general sobre los escenarios en los que puede
usar la coincidencia de patrones. Estas técnicas pueden mejorar la legibilidad y la
corrección del código. Para ver una explicación detallada de todos los patrones que
puede aplicar, consulte el artículo sobre patrones en la referencia del lenguaje.
Comprobaciones de valores null
Uno de los escenarios más comunes para usar la coincidencia de patrones es asegurarse
de que los valores no son null . Puede probar y convertir un tipo de valor que admite
valores null a su tipo subyacente mientras prueba null con el ejemplo siguiente:
C#
int? maybe = 12;
if (maybe is int number)
Console.WriteLine($"The nullable int 'maybe' has the value {number}");
else
Console.WriteLine("The nullable int 'maybe' doesn't hold a value");
El código anterior es un patrón de declaración para probar el tipo de la variable y
asignarlo a una nueva variable. Las reglas de lenguaje hacen que esta técnica sea más
segura que muchas otras. Solo es posible acceder a la variable number y asignarla en la
parte verdadera de la cláusula if . Si intenta acceder a ella en otro lugar, ya sea en la
cláusula else o después del bloque if , el compilador emitirá un error. En segundo
lugar, como no usa el operador == , este patrón funciona cuando un tipo sobrecarga el
operador == . Esto lo convierte en una manera ideal de comprobar los valores de
referencia nula, incorporando el patrón not :
C#
string? message = "This is not the null string";
if (message is not null)
Console.WriteLine(message);
En el ejemplo anterior se ha usado un patrón constante para comparar la variable con
null . not es un patrón lógico que coincide cuando el patrón negado no coincide.
Pruebas de tipo
Otro uso común de la coincidencia de patrones consiste en probar una variable para ver
si coincide con un tipo determinado. Por ejemplo, el código siguiente comprueba si una
variable no es null e implementa la interfaz System.Collections.Generic.IList<T>. Si es así,
usa la propiedad ICollection<T>.Count de esa lista para buscar el índice central. El
patrón de declaración no coincide con un valor null , independientemente del tipo de
tiempo de compilación de la variable. El código siguiente protege contra null , además
de proteger contra un tipo que no implementa IList .
C#
public static T MidPoint<T>(IEnumerable<T> sequence)
if (sequence is IList<T> list)
return list[list.Count / 2];
else if (sequence is null)
throw new ArgumentNullException(nameof(sequence), "Sequence can't be
null.");
else
int halfLength = sequence.Count() / 2 - 1;
if (halfLength < 0) halfLength = 0;
return sequence.Skip(halfLength).First();
Se pueden aplicar las mismas pruebas en una expresión switch para probar una
variable con varios tipos diferentes. Puede usar esa información para crear algoritmos
mejores basados en el tipo de tiempo de ejecución específico.
Comparación de valores discretos
También puede probar una variable para buscar una coincidencia con valores
específicos. En el código siguiente se muestra un ejemplo en el que se prueba un valor
con todos los valores posibles declarados en una enumeración:
C#
public State PerformOperation(Operation command) =>
command switch
Operation.SystemTest => RunDiagnostics(),
Operation.Start => StartSystem(),
Operation.Stop => StopSystem(),
Operation.Reset => ResetToReady(),
_ => throw new ArgumentException("Invalid enum value for command",
nameof(command)),
};
En el ejemplo anterior se muestra un envío de método basado en el valor de una
enumeración. El caso final _ es un patrón de descarte que coincide con todos los
valores. Controla todas las condiciones de error en las que el valor no coincide con uno
de los valores enum definidos. Si omite ese segmento modificador, el compilador le
advierte de que no ha controlado todos los valores de entrada posibles. En tiempo de
ejecución, la expresión switch produce una excepción si el objeto que se está
examinando no coincide con ninguno de los segmentos modificadores. Puede usar
constantes numéricas en lugar de un conjunto de valores de enumeración. También
puede usar esta técnica similar para los valores de cadena constantes que representan
los comandos:
C#
public State PerformOperation(string command) =>
command switch
"SystemTest" => RunDiagnostics(),
"Start" => StartSystem(),
"Stop" => StopSystem(),
"Reset" => ResetToReady(),
_ => throw new ArgumentException("Invalid string value for command",
nameof(command)),
};
En el ejemplo anterior se muestra el mismo algoritmo, pero se usan valores de cadena
en lugar de una enumeración. Usaría este escenario si la aplicación responde a
comandos de texto, en lugar de a un formato de datos normal. A partir de C# 11,
también puede usar Span<char> o ReadOnlySpan<char> para probar los valores de
cadena constantes, tal como se muestra en el ejemplo siguiente:
C#
public State PerformOperation(ReadOnlySpan<char> command) =>
command switch
"SystemTest" => RunDiagnostics(),
"Start" => StartSystem(),
"Stop" => StopSystem(),
"Reset" => ResetToReady(),
_ => throw new ArgumentException("Invalid string value for command",
nameof(command)),
};
En todos estos ejemplos, el patrón de descarte le garantiza que controlará todas las
entradas. El compilador le ayuda a asegurarse de que se controlan todos los valores de
entrada posibles.
Patrones relacionales
Puede usar patrones relacionales para probar cómo se compara un valor con las
constantes. Por ejemplo, el código siguiente devuelve el estado del agua en función de
la temperatura en Fahrenheit:
C#
string WaterState(int tempInFahrenheit) =>
tempInFahrenheit switch
(> 32) and (< 212) => "liquid",
< 32 => "solid",
> 212 => "gas",
32 => "solid/liquid transition",
212 => "liquid / gas transition",
};
El código anterior también muestra el and patrón lógico conjuntivo para comprobar que
ambos patrones relacionales coincidan. También puede usar un patrón or disyuntivo
para comprobar que cualquiera de los patrones coincide. Los dos patrones relacionales
están entre paréntesis, que se pueden usar alrededor de cualquier patrón para mayor
claridad. Los dos últimos segmentos modificadores controlan los casos del punto de
fusión y el punto de ebullición. Sin esos dos segmentos, el compilador le advertirá de
que la lógica no abarca todas las entradas posibles.
El código anterior también muestra otra característica importante que el compilador
proporciona para las expresiones de coincidencia de patrones: el compilador le advierte
si no controla todos los valores de entrada. El compilador emite además una
advertencia si una parte de la expresión switch anterior ya controla una parte de la
expresión. Esto le da libertad para refactorizar y reordenar expresiones switch. La misma
expresión también se podría escribir así:
C#
string WaterState2(int tempInFahrenheit) =>
tempInFahrenheit switch
< 32 => "solid",
32 => "solid/liquid transition",
< 212 => "liquid",
212 => "liquid / gas transition",
_ => "gas",
};
La lección más importante de todo esto y cualquier otra refactorización o reordenación
es que el compilador valida que ha cubierto todas las entradas.
Varias entradas
Todos los patrones que ha visto hasta ahora comprueban una entrada. Puede escribir
patrones que examinen varias propiedades de un objeto. Fíjese en el siguiente registro
Order :
C#
public record Order(int Items, decimal Cost);
El tipo de registro posicional anterior declara dos miembros en posiciones explícitas.
Primero aparece Items y, luego, el valor Cost del pedido. Para obtener más
información, consulte Registros.
El código siguiente examina el número de elementos y el valor de un pedido para
calcular un precio con descuento:
C#
public decimal CalculateDiscount(Order order) =>
order switch
{ Items: > 10, Cost: > 1000.00m } => 0.10m,
{ Items: > 5, Cost: > 500.00m } => 0.05m,
{ Cost: > 250.00m } => 0.02m,
null => throw new ArgumentNullException(nameof(order), "Can't
calculate discount on null order"),
var someObject => 0m,
};
Los dos primeros segmentos examinan dos propiedades de Order . El tercero examina
solo el costo. El siguiente realiza la comprobación de null y el último coincide con
cualquier otro valor. Si el tipo Order define un método Deconstruct adecuado, puede
omitir los nombres de propiedad del patrón y usar la desconstrucción para examinar las
propiedades:
C#
public decimal CalculateDiscount(Order order) =>
order switch
( > 10, > 1000.00m) => 0.10m,
( > 5, > 50.00m) => 0.05m,
{ Cost: > 250.00m } => 0.02m,
null => throw new ArgumentNullException(nameof(order), "Can't
calculate discount on null order"),
var someObject => 0m,
};
El código anterior muestra el patrón posicional donde las propiedades se deconstruyen
para la expresión.
Patrones de lista
Puede comprobar los elementos de una lista o una matriz mediante un patrón de lista.
Un patrón de lista proporciona una forma de aplicar un patrón a cualquier elemento de
una secuencia. Además, puede aplicar el patrón de descarte ( _ ) para que coincida con
cualquier elemento, o bien un patrón de sector para que coincida con ninguno o más
elementos.
Los patrones de lista son una herramienta valiosa cuando los datos no siguen una
estructura normal. Puede usar la coincidencia de patrones para probar la forma y los
valores de los datos en vez de transformarlos en un conjunto de objetos.
Tenga en cuenta el siguiente extracto de un archivo de texto que contiene transacciones
bancarias:
Resultados
04-01-2020, DEPOSIT, Initial deposit, 2250.00
04-15-2020, DEPOSIT, Refund, 125.65
04-18-2020, DEPOSIT, Paycheck, 825.65
04-22-2020, WITHDRAWAL, Debit, Groceries, 255.73
05-01-2020, WITHDRAWAL, #1102, Rent, apt, 2100.00
05-02-2020, INTEREST, 0.65
05-07-2020, WITHDRAWAL, Debit, Movies, 12.57
04-15-2020, FEE, 5.55
Es un formato CSV, pero algunas de las filas tienen más columnas que otras. También
hay algo incluso peor para el procesamiento, una columna del tipo WITHDRAWAL que
tiene texto generado por el usuario y puede contener una coma en el texto. Un patrón
de lista que incluye el patrón de descarte, el patrón constante y el patrón var para
capturar los datos de los procesos del valor en este formato:
C#
decimal balance = 0m;
foreach (string[] transaction in ReadRecords())
balance += transaction switch
[_, "DEPOSIT", _, var amount] => decimal.Parse(amount),
[_, "WITHDRAWAL", .., var amount] => -decimal.Parse(amount),
[_, "INTEREST", var amount] => decimal.Parse(amount),
[_, "FEE", var fee] => -decimal.Parse(fee),
_ => throw new
InvalidOperationException($"Record {string.Join(", ", transaction)} is not
in the expected format!"),
};
Console.WriteLine($"Record: {string.Join(", ", transaction)}, New
balance: {balance:C}");
}
En el ejemplo anterior se toma una matriz de cadenas, donde cada elemento es un
campo de la fila. Las claves de expresión switch del segundo campo, que determinan el
tipo de transacción y el número de columnas restantes. Cada fila garantiza que los datos
están en el formato correcto. El patrón de descarte ( _ ) omite el primer campo, con la
fecha de la transacción. El segundo campo coincide con el tipo de transacción. Las
coincidencias restantes del elemento saltan hasta el campo con la cantidad. La
coincidencia final usa el patrón var para capturar la representación de cadena de la
cantidad. La expresión calcula la cantidad que se va a agregar o restar del saldo.
Los patrones de lista permiten buscar coincidencias en la forma de una secuencia de
elementos de datos. Los patrones de descarte y de sector se usan para hallar
coincidencias con la ubicación de los elementos. Se usan otros patrones para que
coincidan con las características de los elementos individuales.
En este artículo se proporciona una introducción a los tipos de código que se pueden
escribir con la coincidencia de patrones en C#. En los artículos siguientes se muestran
más ejemplos del uso de patrones en escenarios y un vocabulario completo de patrones
disponibles.
Vea también
Uso de la coincidencia de patrones para evitar la comprobación de "is" seguida de
una conversión (reglas de estilo IDE0020 e IDE0038)
Exploración: Uso de la coincidencia de patrones para crear el comportamiento de
la clase y mejorar el código
Tutorial: Uso de la coincidencia de patrones para compilar algoritmos basados en
tipos y basados en datos
Referencia: Coincidencia de patrones
Descartes: aspectos básicos de C#
Artículo • 03/03/2023 • Tiempo de lectura: 9 minutos
Los descartes son variables de marcador de posición que deliberadamente no se usan
en el código de la aplicación. Los descartes son equivalentes a variables sin asignar, ya
que no tienen un valor. Un descarte comunica la intención al compilador y otros
usuarios que leen el código: Pretendía ignorar el resultado de una expresión. Es posible
que desee ignorar el resultado de una expresión, uno o varios miembros de una
expresión de tupla, un parámetro out de un método o el destino de una expresión de
coincidencia de patrones.
Los descartes aclaran la intención del código. Un descarte indica que el código nunca
usa la variable. Mejoran la legibilidad y el mantenimiento.
Para indicar que una variable es un descarte, se le asigna como nombre el carácter de
subrayado ( _ ). Por ejemplo, la siguiente llamada de método devuelve una tupla en la
que el primer y el segundo valor son descartes. area es una variable declarada
previamente establecida en el tercer componente devuelto por GetCityInformation :
C#
(_, _, area) = city.GetCityInformation(cityName);
A partir de C# 9.0, se pueden usar descartes para especificar los parámetros de entrada
de una expresión lambda que no se utilizan. 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.
Cuando _ es un descarte válido, si se intenta recuperar su valor o usarlo en una
operación de asignación, se genera el error del compilador CS0103: "El nombre '_' no
existe en el contexto actual". Este error se debe a que no se le ha asignado un valor a _ ,
y es posible que tampoco se le haya asignado una ubicación de almacenamiento. Si
fuese una variable real no se podría descartar más de un valor, como en el ejemplo
anterior.
Deconstrucción de tuplas y objetos
Los descartes son útiles en el trabajo con tuplas, cuando el código de la aplicación usa
algunos elementos de tupla pero omite otros. Por ejemplo, el siguiente método
QueryCityDataForYears devuelve una tupla con el nombre de una ciudad, su superficie,
un año, la población de la ciudad en ese año, un segundo año y la población de la
ciudad en ese segundo año. En el ejemplo se muestra la evolución de la población entre
esos dos años. De los datos disponibles en la tupla, no nos interesa la superficie de la
ciudad, y conocemos el nombre de la ciudad y las dos fechas en tiempo de diseño.
Como resultado, solo nos interesan los dos valores de población almacenados en la
tupla, y podemos controlar los valores restantes como descartes.
C#
var (_, _, _, pop1, _, pop2) = QueryCityDataForYears("New York City", 1960,
2010);
Console.WriteLine($"Population change, 1960 to 2010: {pop2 - pop1:N0}");
static (string, double, int, int, int, int) QueryCityDataForYears(string
name, int year1, int year2)
int population1 = 0, population2 = 0;
double area = 0;
if (name == "New York City")
area = 468.48;
if (year1 == 1960)
population1 = 7781984;
if (year2 == 2010)
population2 = 8175133;
return (name, area, year1, population1, year2, population2);
return ("", 0, 0, 0, 0, 0);
// The example displays the following output:
// Population change, 1960 to 2010: 393,149
Para obtener más información sobre la deconstrucción de tuplas con descartes, vea
Deconstructing tuples and other types (Deconstruir tuplas y otros tipos).
El método Deconstruct de una clase, estructura o interfaz también permite recuperar y
deconstruir un conjunto de datos específico de un objeto. Puede usar descartes cuando
le interese trabajar con un solo subconjunto de los valores deconstruidos. En el
siguiente ejemplo se deconstruye un objeto Person en cuatro cadenas (el nombre
propio, los apellidos, la ciudad y el estado), pero se descartan los apellidos y el estado.
C#
using System;
namespace Discards
public class Person
public string FirstName { get; set; }
public string MiddleName { get; set; }
public string LastName { get; set; }
public string City { get; set; }
public string State { get; set; }
public Person(string fname, string mname, string lname,
string cityName, string stateName)
FirstName = fname;
MiddleName = mname;
LastName = lname;
City = cityName;
State = stateName;
// Return the first and last name.
public void Deconstruct(out string fname, out string lname)
fname = FirstName;
lname = LastName;
public void Deconstruct(out string fname, out string mname, out
string lname)
fname = FirstName;
mname = MiddleName;
lname = LastName;
public void Deconstruct(out string fname, out string lname,
out string city, out string state)
fname = FirstName;
lname = LastName;
city = City;
state = State;
class Example
public static void Main()
var p = new Person("John", "Quincy", "Adams", "Boston", "MA");
// Deconstruct the person object.
var (fName, _, city, _) = p;
Console.WriteLine($"Hello {fName} of {city}!");
// The example displays the following output:
// Hello John of Boston!
Para obtener más información sobre la deconstrucción de tipos definidos por el usuario
con descartes, vea Deconstructing tuples and other types (Deconstruir tuplas y otros
tipos).
Coincidencia de patrones con switch
El patrón de descarte se puede usar en la coincidencia de patrones con la expresión
switch. Todas las expresiones, incluida null , siempre coinciden con el patrón de
descarte.
En el ejemplo siguiente se define un método ProvidesFormatInfo que usa una expresión
switch para determinar si un objeto proporciona una implementación de
IFormatProvider y probar si el objeto es null . También se usa el patrón de descarte para
controlar los objetos que no son NULL de cualquier otro tipo.
C#
object?[] objects = { CultureInfo.CurrentCulture,
CultureInfo.CurrentCulture.DateTimeFormat,
CultureInfo.CurrentCulture.NumberFormat,
new ArgumentException(), null };
foreach (var obj in objects)
ProvidesFormatInfo(obj);
static void ProvidesFormatInfo(object? obj) =>
Console.WriteLine(obj switch
IFormatProvider fmt => $"{fmt.GetType()} object",
null => "A null object reference: Its use could result in a
NullReferenceException",
_ => "Some object type without format information"
});
// The example displays the following output:
// System.Globalization.CultureInfo object
// System.Globalization.DateTimeFormatInfo object
// System.Globalization.NumberFormatInfo object
// Some object type without format information
// A null object reference: Its use could result in a
NullReferenceException
Llamadas a métodos con parámetros out
Cuando se llama al método Deconstruct para deconstruir un tipo definido por el
usuario (una instancia de una clase, estructura o interfaz), puede descartar los valores de
argumentos out individuales. Pero también puede descartar el valor de argumentos
out al llamar a cualquier método con un parámetro out .
En el ejemplo siguiente se llama al método DateTime.TryParse(String, out DateTime)
para determinar si la representación de cadena de una fecha es válida en la referencia
cultural actual. Dado que al ejemplo solo le interesa validar la cadena de fecha, y no
analizarla para extraer la fecha, el argumento out para el método es un descarte.
C#
string[] dateStrings = {"05/01/2018 14:57:32.8", "2018-05-01 14:57:32.8",
"2018-05-01T14:57:32.8375298-04:00", "5/01/2018",
"5/01/2018 14:57:32.80 -07:00",
"1 May 2018 2:57:32.8 PM", "16-05-2018 1:00:32 PM",
"Fri, 15 May 2018 20:10:57 GMT" };
foreach (string dateString in dateStrings)
if (DateTime.TryParse(dateString, out _))
Console.WriteLine($"'{dateString}': valid");
else
Console.WriteLine($"'{dateString}': invalid");
// The example displays output like the following:
// '05/01/2018 14:57:32.8': valid
// '2018-05-01 14:57:32.8': valid
// '2018-05-01T14:57:32.8375298-04:00': valid
// '5/01/2018': valid
// '5/01/2018 14:57:32.80 -07:00': valid
// '1 May 2018 2:57:32.8 PM': valid
// '16-05-2018 1:00:32 PM': invalid
// 'Fri, 15 May 2018 20:10:57 GMT': invalid
Descarte independiente
Puede usar un descarte independiente para indicar cualquier variable que decida omitir.
Un uso típico es usar una asignación para asegurarse de que un argumento no sea
NULL. En el código siguiente se usa un descarte para forzar una asignación. El lado
derecho de la asignación utiliza el operador de uso combinado de NULL para producir
System.ArgumentNullException cuando el argumento es null . El código no necesita el
resultado de la asignación, por lo que se descarta. La expresión fuerza una
comprobación nula. El descarte aclara su intención: el resultado de la asignación no es
necesario ni se usa.
C#
public static void Method(string arg)
_ = arg ?? throw new ArgumentNullException(paramName: nameof(arg),
message: "arg can't be null");
// Do work with arg.
En el ejemplo siguiente se usa un descarte independiente para omitir el objeto Task
devuelto por una operación asincrónica. La asignación de la tarea tiene el efecto de
suprimir la excepción que se produce en la operación cuando está a punto de
completarse. Hace que su intención sea clara: Quiere descartar Task y omitir los errores
generados a partir de esa operación asincrónica.
C#
private static async Task ExecuteAsyncMethods()
Console.WriteLine("About to launch a task...");
_ = Task.Run(() =>
var iterations = 0;
for (int ctr = 0; ctr < int.MaxValue; ctr++)
iterations++;
Console.WriteLine("Completed looping operation...");
throw new InvalidOperationException();
});
await Task.Delay(5000);
Console.WriteLine("Exiting after 5 second delay");
// The example displays output like the following:
// About to launch a task...
// Completed looping operation...
// Exiting after 5 second delay
Sin asignar la tarea a un descarte, el código siguiente genera una advertencia del
compilador:
C#
private static async Task ExecuteAsyncMethods()
Console.WriteLine("About to launch a task...");
// CS4014: Because this call is not awaited, execution of the current
method continues before the call is completed.
// Consider applying the 'await' operator to the result of the call.
Task.Run(() =>
var iterations = 0;
for (int ctr = 0; ctr < int.MaxValue; ctr++)
iterations++;
Console.WriteLine("Completed looping operation...");
throw new InvalidOperationException();
});
await Task.Delay(5000);
Console.WriteLine("Exiting after 5 second delay");
7 Nota
Si ejecuta cualquiera de los dos ejemplos anteriores mediante un depurador, este
detendrá el programa cuando se produzca la excepción. Sin un depurador
asociado, la excepción se omite en ambos casos en modo silencioso.
_ también es un identificador válido. Cuando se usa fuera de un contexto compatible, _
no se trata como un descarte, sino como una variable válida. Si un identificador
denominado _ ya está en el ámbito, el uso de _ como descarte independiente puede
producir lo siguiente:
La modificación accidental del valor de la variable _ en el ámbito, al asignarle el
valor del descarte previsto. Por ejemplo:
C#
private static void ShowValue(int _)
{
byte[] arr = { 0, 0, 1, 2 };
_ = BitConverter.ToInt32(arr, 0);
Console.WriteLine(_);
// The example displays the following output:
// 33619968
Un error del compilador por infringir la seguridad de tipos. Por ejemplo:
C#
private static bool RoundTrips(int _)
string value = _.ToString();
int newValue = 0;
_ = Int32.TryParse(value, out newValue);
return _ == newValue;
// The example displays the following compiler error:
// error CS0029: Cannot implicitly convert type 'bool' to 'int'
Error del compilador CS0136: "Una variable local o un parámetro denominados '_'
no se pueden declarar en este ámbito porque ese nombre se está usando en un
ámbito local envolvente para definir una variable local o un parámetro". Por
ejemplo:
C#
public void DoSomething(int _)
var _ = GetValue(); // Error: cannot declare local _ when one is
already in scope
// The example displays the following compiler error:
// error CS0136:
// A local or parameter named '_' cannot be declared in this
scope
// because that name is used in an enclosing local scope
// to define a local or parameter
Consulte también
Eliminación de valores de expresión innecesarios (regla de estilo IDE0058)
Eliminación de la asignación de valores innecesarios (regla de estilo IDE0059)
Eliminación del parámetro sin usar (regla de estilo IDE0060)
Deconstruir tuplas y otros tipos
is operator
Expresión switch
Deconstruir tuplas y otros tipos
Artículo • 15/02/2023 • Tiempo de lectura: 11 minutos
Una tupla proporciona una manera ligera de recuperar varios valores de una llamada de
método. Pero una vez que recupere la tupla, deberá controlar sus elementos
individuales. Trabajar elemento a elemento es complicado, como se muestra en el
ejemplo siguiente. El método QueryCityData devuelve una tupla de tres y cada uno de
sus elementos se asigna a una variable en una operación aparte.
C#
public class Example
public static void Main()
var result = QueryCityData("New York City");
var city = result.Item1;
var pop = result.Item2;
var size = result.Item3;
// Do something with the data.
private static (string, int, double) QueryCityData(string name)
if (name == "New York City")
return (name, 8175133, 468.48);
return ("", 0, 0);
Recuperar varios valores de campo y propiedad de un objeto puede ser igualmente
complicado: debe asignar un valor de campo o propiedad a una variable miembro a
miembro.
Puede recuperar varios elementos de una tupla o recuperar varios campos, propiedades
y valores calculados de un objeto en una sola operación de deconstrucción. Para
deconstruir una tupla, asigne sus elementos a variables individuales. Cuando se
deconstruye un objeto, los valores seleccionados se asignan a variables individuales.
Tuplas
C# incluye compatibilidad integrada para deconstruir tuplas, lo que permite
desempaquetar todos los elementos de una tupla en una sola operación. La sintaxis
general para deconstruir una tupla es parecida a la sintaxis para definirla, ya que las
variables a las que se va a asignar cada elemento se escriben entre paréntesis en el lado
izquierdo de una instrucción de asignación. Por ejemplo, la siguiente instrucción asigna
los elementos de una tupla de cuatro a cuatro variables distintas:
C#
var (name, address, city, zip) = contact.GetAddressInfo();
Hay tres formas de deconstruir una tupla:
Se puede declarar explícitamente el tipo de cada campo entre paréntesis. En el
ejemplo siguiente se usa este enfoque para deconstruir la tupla de tres que
devuelve el método QueryCityData .
C#
public static void Main()
(string city, int population, double area) = QueryCityData("New
York City");
// Do something with the data.
Puede usar la palabra clave var para que C# deduzca el tipo de cada variable.
Debe colocar la palabra clave var fuera de los paréntesis. En el ejemplo siguiente
se usa la inferencia de tipos al deconstruir la tupla de tres devuelta por el método
QueryCityData .
C#
public static void Main()
var (city, population, area) = QueryCityData("New York City");
// Do something with the data.
También se puede usar la palabra clave var individualmente con alguna de las
declaraciones de variable, o todas, dentro de los paréntesis.
C#
public static void Main()
(string city, var population, var area) = QueryCityData("New York
City");
// Do something with the data.
Esto es complicado y no se recomienda.
Por último, puede deconstruir la tupla en variables que ya se hayan declarado.
C#
public static void Main()
string city = "Raleigh";
int population = 458880;
double area = 144.8;
(city, population, area) = QueryCityData("New York City");
// Do something with the data.
A partir de C# 10, puede mezclar la declaración y la asignación de variables en una
deconstrucción.
C#
public static void Main()
string city = "Raleigh";
int population = 458880;
(city, population, double area) = QueryCityData("New York City");
// Do something with the data.
No se puede especificar un tipo específico fuera de los paréntesis aunque cada campo
de la tupla tenga el mismo tipo. Al hacerlo, se genera el error del compilador CS8136:
"La forma de deconstrucción "var (...)" no permite un tipo específico para "var'".
Debe asignar cada elemento de la tupla a una variable. Si omite algún elemento, el
compilador genera el error CS8132: "No se puede deconstruir una tupla de "x"
elementos en "y" variables".
Elementos de tupla con descartes
A menudo, cuando se deconstruye una tupla, solo interesan los valores de algunos
elementos. Puede aprovechar la compatibilidad de C# con los descartes, que son
variables de solo escritura cuyos valores se decide omitir. Un carácter de subrayado ("_")
elige un descarte en una asignación. Puede descartar tantos valores como quiera; todos
se representan mediante el descarte único _ .
En el ejemplo siguiente se muestra el uso de tuplas con descartes. El método
QueryCityDataForYears devuelve una tupla de seis con el nombre de una ciudad, su
área, un año, la población de la ciudad en ese año, un segundo año y la población de la
ciudad en ese segundo año. En el ejemplo se muestra la evolución de la población entre
esos dos años. De los datos disponibles en la tupla, no nos interesa la superficie de la
ciudad, y conocemos el nombre de la ciudad y las dos fechas en tiempo de diseño.
Como resultado, solo nos interesan los dos valores de población almacenados en la
tupla, y podemos controlar los valores restantes como descartes.
C#
using System;
public class ExampleDiscard
public static void Main()
var (_, _, _, pop1, _, pop2) = QueryCityDataForYears("New York
City", 1960, 2010);
Console.WriteLine($"Population change, 1960 to 2010: {pop2 -
pop1:N0}");
private static (string, double, int, int, int, int)
QueryCityDataForYears(string name, int year1, int year2)
int population1 = 0, population2 = 0;
double area = 0;
if (name == "New York City")
area = 468.48;
if (year1 == 1960)
population1 = 7781984;
if (year2 == 2010)
population2 = 8175133;
return (name, area, year1, population1, year2, population2);
return ("", 0, 0, 0, 0, 0);
// The example displays the following output:
// Population change, 1960 to 2010: 393,149
Tipos definidos por el usuario
C# no ofrece compatibilidad integrada con la deconstrucción de tipos que no son de
tupla distintos de los tipos record y DictionaryEntry. A pesar de ello, como autor de una
clase, una estructura o una interfaz, puede permitir que las instancias del tipo se
deconstruyan mediante la implementación de uno o varios métodos Deconstruct . El
método no devuelve ningún valor, y cada valor que se va a deconstruir se indica
mediante un parámetro out en la firma del método. Por ejemplo, el siguiente método
Deconstruct de una clase Person devuelve el nombre de pila, el segundo nombre y los
apellidos:
C#
public void Deconstruct(out string fname, out string mname, out string
lname)
A continuación, puede deconstruir una instancia de la clase Person denominada p con
una asignación como el código siguiente:
C#
var (fName, mName, lName) = p;
En el ejemplo siguiente se sobrecarga el método Deconstruct para devolver varias
combinaciones de las propiedades de un objeto Person . Las sobrecargas individuales
devuelven lo siguiente:
El nombre de pila y los apellidos.
El nombre de pila, el segundo nombre y los apellidos.
El nombre de pila, los apellidos, el nombre de la ciudad y el nombre del estado.
C#
using System;
public class Person
public string FirstName { get; set; }
public string MiddleName { get; set; }
public string LastName { get; set; }
public string City { get; set; }
public string State { get; set; }
public Person(string fname, string mname, string lname,
string cityName, string stateName)
FirstName = fname;
MiddleName = mname;
LastName = lname;
City = cityName;
State = stateName;
// Return the first and last name.
public void Deconstruct(out string fname, out string lname)
fname = FirstName;
lname = LastName;
public void Deconstruct(out string fname, out string mname, out string
lname)
fname = FirstName;
mname = MiddleName;
lname = LastName;
public void Deconstruct(out string fname, out string lname,
out string city, out string state)
fname = FirstName;
lname = LastName;
city = City;
state = State;
public class ExampleClassDeconstruction
public static void Main()
var p = new Person("John", "Quincy", "Adams", "Boston", "MA");
// Deconstruct the person object.
var (fName, lName, city, state) = p;
Console.WriteLine($"Hello {fName} {lName} of {city}, {state}!");
// The example displays the following output:
// Hello John Adams of Boston, MA!
Varios métodos de Deconstruct que tienen el mismo número de parámetros son
ambiguos. Debe tener cuidado al definir métodos Deconstruct con distintos números
de parámetros o "aridad". No se pueden distinguir los métodos Deconstruct con el
mismo número de parámetros durante la resolución de sobrecarga.
Tipo definido por el usuario con descartes
Tal como haría con las tuplas, puede usar descartes para omitir los elementos
seleccionados que haya devuelto un método Deconstruct . Cada descarte se define
mediante una variable denominada "_". Una operación de deconstrucción única puede
incluir varios descartes.
En el siguiente ejemplo se deconstruye un objeto Person en cuatro cadenas (el nombre
propio, los apellidos, la ciudad y el estado), pero se descartan los apellidos y el estado.
C#
// Deconstruct the person object.
var (fName, _, city, _) = p;
Console.WriteLine($"Hello {fName} of {city}!");
// The example displays the following output:
// Hello John of Boston!
Métodos de extensión para tipos definidos por
el usuario
Aunque usted no haya creado una clase, una estructura o una interfaz, puede
igualmente deconstruir objetos de ese tipo. Para ello, implemente uno o varios métodos
de extensión Deconstruct que devuelvan los valores que le interesen.
En el ejemplo siguiente se definen dos métodos de extensión Deconstruct para la clase
System.Reflection.PropertyInfo. El primero devuelve un conjunto de valores que indican
las características de la propiedad, incluido su tipo, si es estática o de instancia, si es de
solo lectura y si está indexada. El segundo indica la accesibilidad de la propiedad. Dado
que la accesibilidad de los descriptores de acceso get y set puede diferir, los valores
booleanos indican si la propiedad tiene descriptores de acceso get y set independientes
y, si es así, si tienen la misma accesibilidad. Si solo hay un descriptor de acceso o tanto
el descriptor de acceso get como set tienen la misma accesibilidad, la variable access
indica la accesibilidad de la propiedad en su conjunto. En caso contrario, la accesibilidad
de los descriptores de acceso get y set se indica mediante las variables getAccess y
setAccess .
C#
using System;
using System.Collections.Generic;
using System.Reflection;
public static class ReflectionExtensions
public static void Deconstruct(this PropertyInfo p, out bool isStatic,
out bool isReadOnly, out bool isIndexed,
out Type propertyType)
var getter = p.GetMethod;
// Is the property read-only?
isReadOnly = ! p.CanWrite;
// Is the property instance or static?
isStatic = getter.IsStatic;
// Is the property indexed?
isIndexed = p.GetIndexParameters().Length > 0;
// Get the property type.
propertyType = p.PropertyType;
public static void Deconstruct(this PropertyInfo p, out bool
hasGetAndSet,
out bool sameAccess, out string access,
out string getAccess, out string
setAccess)
hasGetAndSet = sameAccess = false;
string getAccessTemp = null;
string setAccessTemp = null;
MethodInfo getter = null;
if (p.CanRead)
getter = p.GetMethod;
MethodInfo setter = null;
if (p.CanWrite)
setter = p.SetMethod;
if (setter != null && getter != null)
hasGetAndSet = true;
if (getter != null)
if (getter.IsPublic)
getAccessTemp = "public";
else if (getter.IsPrivate)
getAccessTemp = "private";
else if (getter.IsAssembly)
getAccessTemp = "internal";
else if (getter.IsFamily)
getAccessTemp = "protected";
else if (getter.IsFamilyOrAssembly)
getAccessTemp = "protected internal";
if (setter != null)
if (setter.IsPublic)
setAccessTemp = "public";
else if (setter.IsPrivate)
setAccessTemp = "private";
else if (setter.IsAssembly)
setAccessTemp = "internal";
else if (setter.IsFamily)
setAccessTemp = "protected";
else if (setter.IsFamilyOrAssembly)
setAccessTemp = "protected internal";
// Are the accessibility of the getter and setter the same?
if (setAccessTemp == getAccessTemp)
sameAccess = true;
access = getAccessTemp;
getAccess = setAccess = String.Empty;
else
access = null;
getAccess = getAccessTemp;
setAccess = setAccessTemp;
public class ExampleExtension
public static void Main()
Type dateType = typeof(DateTime);
PropertyInfo prop = dateType.GetProperty("Now");
var (isStatic, isRO, isIndexed, propType) = prop;
Console.WriteLine($"\nThe {dateType.FullName}.{prop.Name}
property:");
Console.WriteLine($" PropertyType: {propType.Name}");
Console.WriteLine($" Static: {isStatic}");
Console.WriteLine($" Read-only: {isRO}");
Console.WriteLine($" Indexed: {isIndexed}");
Type listType = typeof(List<>);
prop = listType.GetProperty("Item",
BindingFlags.Public |
BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
var (hasGetAndSet, sameAccess, accessibility, getAccessibility,
setAccessibility) = prop;
Console.Write($"\nAccessibility of the {listType.FullName}.
{prop.Name} property: ");
if (!hasGetAndSet | sameAccess)
Console.WriteLine(accessibility);
else
Console.WriteLine($"\n The get accessor: {getAccessibility}");
Console.WriteLine($" The set accessor: {setAccessibility}");
// The example displays the following output:
// The System.DateTime.Now property:
// PropertyType: DateTime
// Static: True
// Read-only: True
// Indexed: False
//
// Accessibility of the System.Collections.Generic.List`1.Item
property: public
Método de extensión para tipos de sistema
Algunos tipos de sistema proporcionan el método Deconstruct por motivos prácticos.
Por ejemplo, el tipo System.Collections.Generic.KeyValuePair<TKey,TValue> proporciona
esta funcionalidad. Al recorrer en iteración un objeto
System.Collections.Generic.Dictionary<TKey,TValue>, cada elemento es un valor
KeyValuePair<TKey, TValue> y se puede deconstruir. Considere el ejemplo siguiente:
C#
Dictionary<string, int> snapshotCommitMap =
new(StringComparer.OrdinalIgnoreCase)
["https://github.com/dotnet/docs"] = 16_465,
["https://github.com/dotnet/runtime"] = 114_223,
["https://github.com/dotnet/installer"] = 22_436,
["https://github.com/dotnet/roslyn"] = 79_484,
["https://github.com/dotnet/aspnetcore"] = 48_386
};
foreach (var (repo, commitCount) in snapshotCommitMap)
Console.WriteLine(
$"The {repo} repository had {commitCount:N0} commits as of November
10th, 2021.");
Puede agregar un método Deconstruct a tipos de sistema que no tengan uno.
Considere el siguiente método de extensión:
C#
public static class NullableExtensions
public static void Deconstruct<T>(
this T? nullable,
out bool hasValue,
out T value) where T : struct
hasValue = nullable.HasValue;
value = nullable.GetValueOrDefault();
Este método de extensión permite que todos los tipos Nullable<T> se deconstruyan en
una tupla de (bool hasValue, T value) . En el ejemplo siguiente se muestra el código
que usa este método de extensión:
C#
DateTime? questionableDateTime = default;
var (hasValue, value) = questionableDateTime;
Console.WriteLine(
$"{{ HasValue = {hasValue}, Value = {value} }}");
questionableDateTime = DateTime.Now;
(hasValue, value) = questionableDateTime;
Console.WriteLine(
$"{{ HasValue = {hasValue}, Value = {value} }}");
// Example outputs:
// { HasValue = False, Value = 1/1/0001 12:00:00 AM }
// { HasValue = True, Value = 11/10/2021 6:11:45 PM }
Tipos record
Cuando se declara un tipo de registro con dos o más parámetros posicionales, el
compilador crea un método Deconstruct con un parámetro out para cada parámetro
posicional en la declaración de record . Para obtener más información, vea Sintaxis
posicional para la definición de propiedades y Comportamiento de deconstructores en
registros derivados.
Vea también
Deconstrucción de declaraciones de variables (regla de estilo IDE0042)
Descartes
Tipos de tupla
Excepciones y control de excepciones
Artículo • 15/02/2023 • Tiempo de lectura: 3 minutos
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.
C#
public class ExceptionTest
static double SafeDivision(double x, double y)
if (y == 0)
throw new DivideByZeroException();
return x / y;
public static void Main()
// Input for test purposes. Change the values to see
// exception handling behavior.
double a = 98, b = 0;
double result;
try
result = SafeDivision(a, b);
Console.WriteLine("{0} divided by {1} = {2}", a, b, result);
catch (DivideByZeroException)
Console.WriteLine("Attempted divide by zero.");
Información general sobre excepciones
Las excepciones tienen las siguientes propiedades:
Las excepciones son tipos que derivan en última instancia de System.Exception .
Use un bloque try alrededor de las instrucciones que pueden producir
excepciones.
Una vez que se produce una excepción en el bloque try , el flujo de control salta al
primer controlador de excepciones asociado que está presente en cualquier parte
de la pila de llamadas. En C#, la palabra clave catch se utiliza para definir un
controlador de excepciones.
Si no hay ningún controlador de excepciones para una excepción determinada, el
programa deja de ejecutarse con un mensaje de error.
No detecte una excepción a menos que pueda controlarla y dejar la aplicación en
un estado conocido. Si se detecta System.Exception , reinícielo con la palabra clave
throw al final del bloque catch .
Si un bloque catch define una variable de excepción, puede utilizarla para obtener
más información sobre el tipo de excepción que se ha producido.
Las excepciones puede generarlas explícitamente un programa con la palabra
clave throw .
Los objetos de excepción contienen información detallada sobre el error, como el
estado de la pila de llamadas y una descripción de texto del error.
El código de un bloque finally se ejecuta incluso si se produce una excepción.
Use un bloque finally para liberar recursos, por ejemplo, para cerrar las
secuencias o los archivos que se abrieron en el bloque try .
Las excepciones administradas de .NET se implementan en el mecanismo de
control de excepciones estructurado de Win32. Para más información, vea Control
de excepciones estructurado (C/C++) y A Crash Course on the Depths of Win32
Structured Exception Handling (Curso intensivo sobre los aspectos específicos
del control de excepciones estructurado de Win32).
Especificación del lenguaje C#
Para obtener más información, consulte la sección Excepciones de Especificación del
lenguaje C#. La especificación del lenguaje es la fuente definitiva de la sintaxis y el uso
de C#.
Vea también
SystemException
Palabras clave de C#
throw
try-catch
try-finally
try-catch-finally
Excepciones
Uso de excepciones
Artículo • 15/02/2023 • Tiempo de lectura: 3 minutos
En C#, los errores del programa en tiempo de ejecución se propagan a través del
programa mediante un mecanismo denominado excepciones. Las excepciones las inicia
el código que encuentra un error y las detecta el código que puede corregir dicho error.
El entorno de ejecución .NET o el código de un programa pueden producir excepciones.
Una vez iniciada, una excepción se propaga hasta la pila de llamadas hasta que
encuentra una instrucción catch para la excepción. Las excepciones no detectadas se
controlan mediante un controlador de excepciones que ofrece el sistema y muestra un
cuadro de diálogo.
Las excepciones están representadas por clases derivadas de Exception. Esta clase
identifica el tipo de excepción y contiene propiedades que tienen los detalles sobre la
excepción. Iniciar una excepción implica crear una instancia de una clase derivada de
excepción, configurar opcionalmente las propiedades de la excepción y luego producir
el objeto con la palabra clave throw . Por ejemplo:
C#
class CustomException : Exception
public CustomException(string message)
private static void TestThrow()
throw new CustomException("Custom exception in TestThrow()");
Cuando se inicia una excepción, el entorno runtime comprueba la instrucción actual
para ver si se encuentra dentro de un bloque try . Si es así, se comprueban los bloques
catch asociados al bloque try para ver si pueden detectar la excepción. Los bloques
Catch suelen especificar tipos de excepción; si el tipo del bloque catch es el mismo de
la excepción, o una clase base de la excepción, el bloque catch puede controlar el
método. Por ejemplo:
C#
try
TestThrow();
catch (CustomException ex)
System.Console.WriteLine(ex.ToString());
Si la instrucción que inicia una excepción no está en un bloque try , o si el bloque try
que la encierra no tiene un elemento catch coincidente, el entorno de ejecución busca
una instrucción try y bloques catch en el método de llamada. El entorno runtime sigue
hasta la pila de llamadas para buscar un bloque catch compatible. Después de
encontrar el bloque catch y ejecutarlo, el control pasa a la siguiente instrucción
después de dicho bloque catch .
Una instrucción try puede contener más de un bloque catch . Se ejecuta la primera
instrucción catch que pueda controlar la excepción; las instrucciones catch siguientes
se omiten, aunque sean compatibles. Ordene los bloques catch de más específicos (o
más derivados) a menos específicos. Por ejemplo:
C#
using System;
using System.IO;
namespace Exceptions
public class CatchOrder
public static void Main()
try
using (var sw = new StreamWriter("./test.txt"))
sw.WriteLine("Hello");
// Put the more specific exceptions first.
catch (DirectoryNotFoundException ex)
Console.WriteLine(ex);
catch (FileNotFoundException ex)
Console.WriteLine(ex);
// Put the least specific exception last.
catch (IOException ex)
Console.WriteLine(ex);
Console.WriteLine("Done");
Para que el bloque catch se ejecute, el entorno runtime busca bloques finally . Los
bloques Finally permiten al programador limpiar cualquier estado ambiguo que
pudiera haber quedado tras la anulación de un bloque try o liberar los recursos
externos (como identificadores de gráficos, conexiones de base de datos o flujos de
archivos) sin tener que esperar a que el recolector de elementos no utilizados en el
entorno de ejecución finalice los objetos. Por ejemplo:
C#
static void TestFinally()
FileStream? file = null;
//Change the path to something that works on your machine.
FileInfo fileInfo = new System.IO.FileInfo("./file.txt");
try
file = fileInfo.OpenWrite();
file.WriteByte(0xF);
finally
// Closing the file allows you to reopen it immediately - otherwise
IOException is thrown.
file?.Close();
try
file = fileInfo.OpenWrite();
Console.WriteLine("OpenWrite() succeeded");
catch (IOException)
Console.WriteLine("OpenWrite() failed");
Si WriteByte() ha iniciado una excepción, el código del segundo bloque try que
intente reabrir el archivo generaría un error si no se llama a file.Close() , y el archivo
permanecería bloqueado. Como los bloques finally se ejecutan aunque se inicie una
excepción, el bloque finally del ejemplo anterior permite que el archivo se cierre
correctamente y ayuda a evitar un error.
Si no se encuentra ningún bloque catch compatible en la pila de llamadas después de
iniciar una excepción, sucede una de estas tres acciones:
Si la excepción se encuentra en un finalizador, este se anula y, si procede, se llama
al finalizador base.
Si la pila de llamadas contiene un constructor estático o un inicializador de campo
estático, se inicia una excepción TypeInitializationException, y la excepción original
se asigna a la propiedad InnerException de la nueva excepción.
Si se llega al comienzo del subproceso, este finaliza.
Control de excepciones (Guía de
programación de C#)
Artículo • 15/02/2023 • Tiempo de lectura: 5 minutos
Los programadores de C# usan un bloque try para separar el código que podría verse
afectado por una excepción. Los bloques catch asociados se usan para controlar las
excepciones resultantes. Un bloque finally contiene código que se ejecuta
independientemente de si se produce una excepción en el bloque try , como la
liberación de recursos asignados en el bloque try . Los bloques try requieren uno o
varios bloques catch asociados, un bloque finally o ambos.
En los ejemplos siguientes se muestra una instrucción try-catch , una instrucción try-
finally y una instrucción try-catch-finally .
C#
try
// Code to try goes here.
catch (SomeSpecificException ex)
// Code to handle the exception goes here.
// Only catch exceptions that you know how to handle.
// Never catch base class System.Exception without
// rethrowing it at the end of the catch block.
C#
try
// Code to try goes here.
finally
// Code to execute after the try block goes here.
C#
try
// Code to try goes here.
catch (SomeSpecificException ex)
// Code to handle the exception goes here.
finally
// Code to execute after the try (and possibly catch) blocks
// goes here.
Un bloque try sin un bloque catch o finally produce un error del compilador.
Bloques catch
Los bloques catch pueden especificar el tipo de excepción que quiere detectar. La
especificación de tipo se denomina filtro de excepciones. El tipo de excepción se debe
derivar de Exception. En general, no especifique Exception como el filtro de excepciones
a menos que sepa cómo controlar todas las que puedan producirse en el bloque try o
que haya incluido una instrucción throw al final del bloque catch .
Se pueden encadenar juntos varios bloques catch con distintas clases de excepciones.
Los bloques catch se evalúan de arriba abajo en el código, pero solo se ejecuta un
bloque catch para cada excepción que se produce. Se ejecuta el primer bloque catch
que especifica el tipo exacto o una clase base de la excepción producida. Si no hay
ningún bloque catch que especifique una clase de excepciones coincidente, se
selecciona un bloque catch que no tenga ningún tipo, si hay alguno en la instrucción.
Es importante colocar primero los bloques catch con las clases de excepción más
específicas (es decir, las más derivadas).
Detecte excepciones cuando se cumplan las condiciones siguientes:
Comprende bien el motivo por el que podría producirse la excepción y puede
implementar una recuperación específica, por ejemplo, pedir al usuario que escriba
un nuevo nombre de archivo cuando detecte un objeto FileNotFoundException.
Puede crear y producir una nueva excepción más específica.
C#
int GetInt(int[] array, int index)
try
return array[index];
catch (IndexOutOfRangeException e)
throw new ArgumentOutOfRangeException(
"Parameter index is out of range.", e);
Quiere controlar parcialmente una excepción antes de pasarla para aumentar su
control. En el ejemplo siguiente, se usa un bloque catch para agregar una entrada
a un registro de errores antes de volver a producir la excepción.
C#
try
// Try to access a resource.
catch (UnauthorizedAccessException e)
// Call a custom error logging procedure.
LogError(e);
// Re-throw the error.
throw;
También puede especificar filtros de excepción para agregar una expresión booleana a
una cláusula catch. Los filtros de excepción indican que una cláusula catch específica
solo coincide cuando la condición es "true". En el ejemplo siguiente, ambas cláusulas
catch usan la misma clase de excepción, pero se comprueba una condición adicional
para crear un mensaje de error distinto:
C#
int GetInt(int[] array, int index)
try
return array[index];
catch (IndexOutOfRangeException e) when (index < 0)
throw new ArgumentOutOfRangeException(
"Parameter index cannot be negative.", e);
catch (IndexOutOfRangeException e)
throw new ArgumentOutOfRangeException(
"Parameter index cannot be greater than the array size.", e);
Se puede usar un filtro de excepción que siempre devuelva false para examinar todas
las excepciones, pero no para procesarlas. Un uso habitual es registrar excepciones:
C#
public static void Main()
try
string? s = null;
Console.WriteLine(s.Length);
catch (Exception e) when (LogException(e))
Console.WriteLine("Exception must have been handled");
private static bool LogException(Exception e)
Console.WriteLine($"\tIn the log routine. Caught {e.GetType()}");
Console.WriteLine($"\tMessage: {e.Message}");
return false;
El método LogException siempre devuelve false ; ninguna cláusula catch que use este
filtro de excepción coincide. La cláusula catch puede ser general, mediante el uso de
System.Exception, y las cláusulas posteriores pueden procesar clases de excepción más
específicas.
Bloques Finally
Los bloques finally permiten limpiar las acciones que se realizan en un bloque try . Si
está presente, el bloque finally se ejecuta en último lugar, después del bloque try y
de cualquier bloque catch coincidente. Un bloque finally siempre se ejecuta,
independientemente de si se produce una excepción o si se encuentra un bloque catch
que coincida con el tipo de excepción.
Los bloques finally pueden usarse para liberar recursos como secuencias de archivo,
conexiones de base de datos y controladores de gráficos sin necesidad de esperar a que
el recolector de elementos no utilizados en tiempo de ejecución finalice los objetos.
Para obtener más información, vea using (instrucción).
En el ejemplo siguiente, el bloque finally se usa para cerrar un archivo que se abre en
el bloque try . Observe que se comprueba el estado del identificador de archivos antes
de cerrar el archivo. Si el bloque try no puede abrir el archivo, el manipulador de
archivos sigue teniendo el valor null , y el bloque finally no intenta cerrarlo. En lugar
de eso, si el archivo se abre correctamente en el bloque try , el bloque finally cierra el
archivo abierto.
C#
FileStream? file = null;
FileInfo fileinfo = new System.IO.FileInfo("./file.txt");
try
file = fileinfo.OpenWrite();
file.WriteByte(0xF);
finally
// Check for null because OpenWrite might have failed.
file?.Close();
Especificación del lenguaje C#
Para obtener más información, vea las secciones Excepciones y La instrucción try de la
Especificación del lenguaje C#. La especificación del lenguaje es la fuente definitiva de la
sintaxis y el uso de C#.
Consulte también
Referencia de C#
try-catch
try-finally
try-catch-finally
using (instrucción)
Creación y producción de excepciones
Artículo • 15/02/2023 • Tiempo de lectura: 4 minutos
Las excepciones se usan para indicar que se ha producido un error mientras se
ejecutaba el programa. Se crean los objetos de excepción que describen un error y,
luego, se producen con la palabra clave throw. Después, el tiempo de ejecución busca el
controlador de excepciones más compatible.
Los programadores deberían producir excepciones cuando una o varias de las
siguientes condiciones sean verdaderas:
El método no puede finalizar su función definida. Por ejemplo, si un parámetro de
un método tiene un valor no válido:
C#
static void CopyObject(SampleClass original)
_ = original ?? throw new ArgumentException("Parameter cannot be
null", nameof(original));
Se realiza una llamada inadecuada a un objeto, en función del estado del objeto.
Un ejemplo podría ser intentar escribir en un archivo de solo lectura. En los casos
en los que un estado de objeto no permite una operación, genere una instancia de
InvalidOperationException o un objeto con base en una derivación de esta clase. El
código siguiente es un ejemplo de un método que genera un objeto
InvalidOperationException:
C#
public class ProgramLog
{
FileStream logFile = null!;
public void OpenLog(FileInfo fileName, FileMode mode) { }
public void WriteLog()
if (!logFile.CanWrite)
throw new InvalidOperationException("Logfile cannot be
read-only");
// Else write data to the log and return.
Cuando un argumento de un método genera una excepción. En este caso, se debe
detectar la excepción original y se debe crear una instancia de ArgumentException.
La excepción original debe pasarse al constructor de ArgumentException como el
parámetro InnerException:
C#
static int GetValueFromArray(int[] array, int index)
try
return array[index];
catch (IndexOutOfRangeException e)
throw new ArgumentOutOfRangeException(
"Parameter index is out of range.", e);
7 Nota
El ejemplo anterior es para fines ilustrativos. La validación de índices a través de
excepciones es, en la mayoría de los casos, una práctica incorrecta. Las excepciones
deben estar reservadas para protegerse frente a condiciones de programa
excepcionales, no para la comprobación de argumentos que se ha visto antes.
Las excepciones contienen una propiedad denominada StackTrace. Esta cadena contiene
el nombre de los métodos de la pila de llamadas actual, junto con el nombre de archivo
y el número de la línea en la que se ha producido la excepción para cada método.
Common Language Runtime (CLR) crea automáticamente un objeto StackTrace desde el
punto de la instrucción throw , de manera que todas las excepciones se deben producir
desde el punto en el que debe comenzar el seguimiento de la pila.
Todas las excepciones contienen una propiedad denominada Message. Esta cadena
debe establecerse para que explique el motivo de la excepción. No se debe colocar
información confidencial en materia de seguridad en el texto del mensaje. Además de
Message, ArgumentException contiene una propiedad denominada ParamName que
debe establecerse en el nombre del argumento que ha provocado que se genere la
excepción. En un establecedor de propiedades, ParamName debe establecerse en
value .
Los métodos públicos y protegidos generan excepciones siempre que no puedan
finalizar sus funciones previstas. La clase de excepciones generada es la excepción más
específica disponible que se ajuste a las condiciones de error. Estas excepciones se
deben documentar como parte de la funcionalidad de la clase, y las clases derivadas o
actualizaciones de la clase original deben mantener el mismo comportamiento para la
compatibilidad con versiones anteriores.
Aspectos que se deben evitar al producir
excepciones
En la siguiente lista se identifican los procedimientos que se deben evitar al producir
excepciones:
No use excepciones para cambiar el flujo de un programa como parte de la
ejecución normal. Use excepciones para notificar y controlar condiciones de error.
Las excepciones no se deben devolver como un parámetro o valor devuelto en
lugar de producirse.
No genere System.Exception, System.SystemException,
System.NullReferenceException ni System.IndexOutOfRangeException de manera
intencionada desde su propio código fuente.
No cree excepciones que se puedan producir en el modo de depuración, pero no
en el modo de lanzamiento. Para identificar los errores en tiempo de ejecución
durante la fase de desarrollo, use la aserción de depuración.
Definir clases de excepción
Los programas pueden producir una clase de excepción predefinida en el espacio de
nombres System (excepto en los casos indicados anteriormente) o crear sus propias
clases de excepción mediante la derivación de Exception. Las clases derivadas deben
definir al menos tres constructores: un constructor sin parámetros, uno que establezca la
propiedad de mensaje y otro que establezca las propiedades Message y InnerException.
Por ejemplo:
C#
[Serializable]
public class InvalidDepartmentException : Exception
public InvalidDepartmentException() : base() { }
public InvalidDepartmentException(string message) : base(message) { }
public InvalidDepartmentException(string message, Exception inner) :
base(message, inner) { }
Agregue propiedades nuevas a la clase de excepción cuando los datos que
proporcionan sean útiles para resolver la excepción. Si se agregan nuevas propiedades a
la clase de excepción derivada, se debe invalidar ToString() para devolver la
información agregada.
Especificación del lenguaje C#
Para obtener más información, vea las secciones Excepciones y La instrucción throw de
la Especificación del lenguaje C#. La especificación del lenguaje es la fuente definitiva de
la sintaxis y el uso de C#.
Vea también
Jerarquía de excepciones
Excepciones generadas por el
compilador
Artículo • 15/02/2023 • Tiempo de lectura: 2 minutos
Algunas excepciones las inicia automáticamente el entorno de ejecución .NET cuando se
producen errores de operaciones básicas. En la tabla siguiente se enumeran estas
excepciones y sus condiciones de error.
Excepción Descripción
ArithmeticException Una clase base para las excepciones que se producen durante las
operaciones aritméticas, como DivideByZeroException y
OverflowException.
ArrayTypeMismatchException Se inicia cuando una matriz no puede almacenar un elemento
determinado porque el tipo real del elemento es incompatible
con el tipo real de la matriz.
DivideByZeroException Se inicia cuando se intenta dividir un valor entero entre cero.
IndexOutOfRangeException Se inicia cuando se intenta indexar una matriz y el índice es
menor que cero o queda fuera de los límites de la matriz.
InvalidCastException Se inicia cuando se produce un error, en tiempo de ejecución, en
una conversión explícita de un tipo base a una interfaz o a un tipo
derivado.
NullReferenceException Se inicia cuando se intenta hacer referencia a un objeto cuyo
valor es null.
OutOfMemoryException Se inicia cuando se produce un error al intentar asignar memoria
con el operador new. Esta excepción indica que se ha agotado la
memoria disponible para el entorno compatible con Common
Language Runtime.
OverflowException Se inicia cuando se desborda una operación aritmética en un
contexto checked .
StackOverflowException Se inicia cuando se agota la pila de ejecución por tener
demasiadas llamadas a métodos pendientes. Normalmente,
indica una recursividad muy profunda o infinita.
TypeInitializationException Se inicia cuando un constructor estático inicia una excepción y no
existe una cláusula catch compatible para capturarla.
Vea también
try-catch
try-finally
try-catch-finally
Convenciones y reglas de nomenclatura
de identificadores de C#
Artículo • 29/11/2022 • Tiempo de lectura: 2 minutos
Un identificador es el nombre que se asigna a un tipo (clase, interfaz, estructura,
registro, delegado o enumeración), miembro, variable o espacio de nombres.
Reglas de nomenclatura
Los identificadores válidos deben seguir estas reglas:
Los identificadores deben comenzar con una letra o un carácter de subrayado ( _ ).
Los identificadores pueden contener caracteres de letra Unicode, caracteres de
dígito decimales, caracteres de conexión Unicode, caracteres de combinación
Unicode o caracteres de formato Unicode. Para obtener más información sobre las
categorías Unicode, vea la base de datos de categorías Unicode .
Puede declarar
identificadores que coincidan con palabras clave de C# mediante el prefijo @ en el
identificador. @ no forma parte del nombre de identificador. Por ejemplo, @if
declara un identificador denominado if . Estos identificadores textuales son
principalmente para la interoperabilidad con los identificadores declarados en
otros lenguajes.
Para obtener una definición completa de identificadores válidos, vea el tema
Identificadores en la Especificación del lenguaje C#.
Convenciones de nomenclatura
Además de las reglas, hay muchas convenciones de nomenclatura de identificadores
que se utilizan en las API de .NET. Por convención, los programas de C# usan
PascalCase para nombres de tipo, espacios de nombres y todos los miembros públicos.
Además, son comunes las convenciones siguientes:
Los nombres de interfaz empiezan por una I mayúscula.
Los tipos de atributo terminan con la palabra Attribute .
Los tipos de enumeración usan un sustantivo singular para los que no son marcas
y uno plural para los que sí.
Los identificadores no deben contener dos caracteres de subrayado ( _ )
consecutivos. Esos nombres están reservados para los identificadores generados
por el compilador.
Para más información, consulte Convenciones de nomenclatura.
Especificación del lenguaje C#
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#.
Consulte también
Guía de programación de C#
Referencia de C#
Clases
Tipos de estructura
Espacios de nombres
Interfaces
Delegados
Convenciones de código de C#
Artículo • 03/03/2023 • Tiempo de lectura: 13 minutos
Las convenciones de codificación tienen los objetivos siguientes:
" Crean una apariencia coherente en el código, para que los lectores puedan
centrarse en el contenido, no en el diseño.
" Permiten a los lectores comprender el código más rápidamente al hacer
suposiciones basadas en la experiencia anterior.
" Facilitan la copia, el cambio y el mantenimiento del código.
" Muestran los procedimientos recomendados de C#.
) Importante
Microsoft usa las instrucciones de este artículo para desarrollar ejemplos y
documentación. Se adoptaron a partir de las instrucciones de estilo de codificación
de .NET Runtime y C# . Puede usarlas o adaptarlas a sus necesidades. Los
objetivos principales son la coherencia y la legibilidad dentro del proyecto, el
equipo, la organización o el código fuente de la empresa.
Convenciones de nomenclatura
Hay varias convenciones de nomenclatura que se deben tener en cuenta al escribir
código de C#.
En los ejemplos siguientes, cualquiera de las instrucciones relativas a los elementos
marcados public también es aplicable cuando se trabaja con elementos protected y
protected internal , los cuales están diseñados para ser visibles para los autores de
llamadas externos.
Pascal case
Use la grafía Pascal ("PascalCasing") al asignar un nombre a class , record o struct .
C#
public class DataService
C#
public record PhysicalAddress(
string Street,
string City,
string StateOrProvince,
string ZipCode);
C#
public struct ValueCoordinate
Al asignar un nombre a interface , use la grafía Pascal además de agregar el prefijo I al
nombre. Esto indica claramente a los consumidores que es un elemento interface .
C#
public interface IWorkerQueue
Al asignar nombres a miembros public de tipos, como campos, propiedades, eventos,
métodos y funciones locales, use la grafía Pascal.
C#
public class ExampleEvents
// A public field, these should be used sparingly
public bool IsValid;
// An init-only property
public IWorkerQueue WorkerQueue { get; init; }
// An event
public event Action EventProcessing;
// Method
public void StartEventProcessing()
// Local function
static int CountQueueItems() => WorkerQueue.Count;
// ...
Al escribir registros posicionales, use la grafía Pascal para los parámetros, ya que son las
propiedades públicas del registro.
C#
public record PhysicalAddress(
string Street,
string City,
string StateOrProvince,
string ZipCode);
Para más información sobre los registros posicionales, consulte Sintaxis posicional para
la definición de propiedades.
Grafía Camel
Use la grafía Camel ("camelCasing") al asignar nombres a campos private o internal , y
prefijos con _ .
C#
public class DataService
private IWorkerQueue _workerQueue;
Sugerencia
Al editar código de C# que sigue estas convenciones de nomenclatura en un IDE
que admite la finalización de instrucciones, al escribir _ se mostrarán todos los
miembros con ámbito de objeto.
Al trabajar con campos static que sean private o internal , use el prefijo s_ y, para el
subproceso estático, use t_ .
C#
public class DataService
private static IWorkerQueue s_workerQueue;
[ThreadStatic]
private static TimeSpan t_timeSpan;
Al escribir parámetros de método, use la grafía Camel.
C#
public T SomeMethod<T>(int someNumber, bool isValid)
Para obtener más información sobre las convenciones de nomenclatura de C#, vea Estilo
de codificación de C# .
Convenciones de nomenclatura adicionales
En ejemplos que no incluyan directivas using, use calificaciones de espacio de
nombres. Si sabe que un espacio de nombres se importa en un proyecto de forma
predeterminada, no es necesario completar los nombres de ese espacio de
nombres. Los nombres completos pueden partirse después de un punto (.) si son
demasiado largos para una sola línea, como se muestra en el ejemplo siguiente.
C#
var currentPerformanceCounterCategory = new System.Diagnostics.
PerformanceCounterCategory();
No es necesario cambiar los nombres de objetos que se crearon con las
herramientas del diseñador de Visual Studio para que se ajusten a otras directrices.
Convenciones de diseño
Un buen diseño utiliza un formato que destaque la estructura del código y haga que el
código sea más fácil de leer. Las muestras y ejemplos de Microsoft cumplen las
convenciones siguientes:
Utilice la configuración del Editor de código predeterminada (sangría automática,
sangrías de 4 caracteres, tabulaciones guardadas como espacios). Para obtener
más información, vea Opciones, editor de texto, C#, formato.
Escriba solo una instrucción por línea.
Escriba solo una declaración por línea.
Si a las líneas de continuación no se les aplica sangría automáticamente, hágalo
con una tabulación (cuatro espacios).
Agregue al menos una línea en blanco entre las definiciones de método y las de
propiedad.
Utilice paréntesis para que las cláusulas de una expresión sean evidentes, como se
muestra en el código siguiente.
C#
if ((val1 > val2) && (val1 > val3))
// Take appropriate action.
Colocación de directivas using fuera de la
declaración del espacio de nombres
Cuando una directiva using está fuera de la declaración de un espacio de nombres, ese
espacio de nombres importado es su nombre completo. Eso es más claro. Cuando la
directiva using está dentro del espacio de nombres, puede ser relativa al espacio de
nombres o su nombre completo. Eso es ambiguo.
C#
using Azure;
namespace CoolStuff.AwesomeFeature
public class Awesome
public void Stuff()
WaitUntil wait = WaitUntil.Completed;
Se supone que hay una referencia (directa o indirecta) a la clase WaitUntil.
Ahora vamos a cambiarla ligeramente:
C#
namespace CoolStuff.AwesomeFeature
using Azure;
public class Awesome
public void Stuff()
WaitUntil wait = WaitUntil.Completed;
Y se compila hoy. Y mañana. Sin embargo, en algún momento de la próxima semana,
este código (sin modificar) produce dos errores:
Consola
- error CS0246: The type or namespace name 'WaitUntil' could not be found
(are you missing a using directive or an assembly reference?)
- error CS0103: The name 'WaitUntil' does not exist in the current context
Una de las dependencias ha introducido esta clase en un espacio de nombres y,
después, finaliza con .Azure :
C#
namespace CoolStuff.Azure
public class SecretsManagement
public string FetchFromKeyVault(string vaultId, string secretId) {
return null; }
Una directiva using colocada dentro de un espacio de nombres es contextual y
complica la resolución de nombres. En este ejemplo, es el primer espacio de nombres
que encuentra.
CoolStuff.AwesomeFeature.Azure
CoolStuff.Azure
Azure
Si se agrega un nuevo espacio de nombres que coincida con CoolStuff.Azure o
CoolStuff.AwesomeFeature.Azure , se tomaría como coincidencia antes que el espacio de
nombres global Azure . Para resolverlo, agregue el modificador global:: a la
declaración using . Sin embargo, es más fácil colocar declaraciones using fuera del
espacio de nombres.
C#
namespace CoolStuff.AwesomeFeature
using global::Azure;
public class Awesome
public void Stuff()
WaitUntil wait = WaitUntil.Completed;
Convenciones de los comentarios
Coloque el comentario en una línea independiente, no al final de una línea de
código.
Comience el texto del comentario con una letra mayúscula.
Finalice el texto del comentario con un punto.
Inserte un espacio entre el delimitador de comentario (//) y el texto del
comentario, como se muestra en el ejemplo siguiente.
C#
// The following declaration creates a query. It does not run
// the query.
No crees bloques de formato con asteriscos alrededor de los comentarios.
Asegúrese de que todos los miembros públicos tengan los comentarios XML
necesarios que proporcionan descripciones adecuadas sobre su comportamiento.
Convenciones de lenguaje
En las secciones siguientes se describen las prácticas que sigue el equipo C# para
preparar las muestras y ejemplos de código.
Tipo de datos String
Use interpolación de cadenas para concatenar cadenas cortas, como se muestra en
el código siguiente.
C#
string displayName = $"{nameList[n].LastName},
{nameList[n].FirstName}";
Para anexar cadenas en bucles, especialmente cuando se trabaja con grandes
cantidades de texto, utilice un objeto StringBuilder.
C#
var phrase =
"lalalalalalalalalalalalalalalalalalalalalalalalalalalalalala";
var manyPhrases = new StringBuilder();
for (var i = 0; i < 10000; i++)
manyPhrases.Append(phrase);
//Console.WriteLine("tra" + manyPhrases);
Variables locales con tipo implícito
Use tipos implícitos para las variables locales cuando el tipo de la variable sea
obvio desde el lado derecho de la asignación, o cuando el tipo exacto no sea
importante.
C#
var var1 = "This is clearly a string.";
var var2 = 27;
No use var cuando el tipo no sea evidente desde el lado derecho de la asignación.
No asuma que el tipo está claro a partir de un nombre de método. Se considera
que un tipo de variable es claro si es un operador new o una conversión explícita.
C#
int var3 = Convert.ToInt32(Console.ReadLine());
int var4 = ExampleClass.ResultSoFar();
No confíe en el nombre de variable para especificar el tipo de la variable. Puede no
ser correcto. En el siguiente ejemplo, el nombre de la variable inputInt es
engañoso. Es una cadena.
C#
var inputInt = Console.ReadLine();
Console.WriteLine(inputInt);
Evite el uso de var en lugar de dynamic. Use dynamic cuando desee la inferencia
de tipos en tiempo de ejecución. Para obtener más información, vea Uso de tipo
dinámico (Guía de programación de C#).
Use tipos implícitos para determinar el tipo de la variable de bucle en bucles for.
En el ejemplo siguiente se usan tipos implícitos en una instrucción for .
C#
var phrase =
"lalalalalalalalalalalalalalalalalalalalalalalalalalalalalala";
var manyPhrases = new StringBuilder();
for (var i = 0; i < 10000; i++)
manyPhrases.Append(phrase);
//Console.WriteLine("tra" + manyPhrases);
No use tipos implícitos para determinar el tipo de la variable de bucle en bucles
foreach. En la mayoría de los casos, el tipo de elementos de la colección no es
inmediatamente obvio. El nombre de la colección no debe servir únicamente para
inferir el tipo de sus elementos.
En el ejemplo siguiente se usan tipos explícitos en una instrucción foreach .
C#
foreach (char ch in laugh)
if (ch == 'h')
Console.Write("H");
else
Console.Write(ch);
Console.WriteLine();
7 Nota
Tenga cuidado de no cambiar accidentalmente un tipo de elemento de la
colección iterable. Por ejemplo, es fácil cambiar de System.Linq.IQueryable a
System.Collections.IEnumerable en una instrucción foreach , lo cual cambia la
ejecución de una consulta.
Tipos de datos sin signo
En general, utilice int en lugar de tipos sin signo. El uso de int es común en todo C#, y
es más fácil interactuar con otras bibliotecas cuando se usa int .
Matrices
Utilice sintaxis concisa para inicializar las matrices en la línea de declaración. En el
siguiente ejemplo, observe que no puede utilizar var en lugar de string[] .
C#
string[] vowels1 = { "a", "e", "i", "o", "u" };
Si usa la creación de instancias explícita, puede usar var .
C#
var vowels2 = new string[] { "a", "e", "i", "o", "u" };
Delegados
Use Func<> y Action<> en lugar de definir tipos de delegado. En una clase, defina el
método delegado.
C#
public static Action<string> ActionExample1 = x => Console.WriteLine($"x is:
{x}");
public static Action<string, string> ActionExample2 = (x, y) =>
Console.WriteLine($"x is: {x}, y is {y}");
public static Func<string, int> FuncExample1 = x => Convert.ToInt32(x);
public static Func<int, int, int> FuncExample2 = (x, y) => x + y;
Llame al método con la signatura definida por el delegado Func<> o Action<> .
C#
ActionExample1("string for x");
ActionExample2("string for x", "string for y");
Console.WriteLine($"The value is {FuncExample1("1")}");
Console.WriteLine($"The sum is {FuncExample2(1, 2)}");
Si crea instancias de un tipo de delegado, utilice la sintaxis concisa. En una clase, defina
el tipo de delegado y un método que tenga una firma coincidente.
C#
public delegate void Del(string message);
public static void DelMethod(string str)
Console.WriteLine("DelMethod argument: {0}", str);
Cree una instancia del tipo de delegado y llámela. La siguiente declaración muestra la
sintaxis condensada.
C#
Del exampleDel2 = DelMethod;
exampleDel2("Hey");
La siguiente declaración utiliza la sintaxis completa.
C#
Del exampleDel1 = new Del(DelMethod);
exampleDel1("Hey");
Instrucciones try - catch y using en el control de
excepciones
Use una instrucción try-catch en la mayoría de casos de control de excepciones.
C#
static string GetValueFromArray(string[] array, int index)
try
return array[index];
catch (System.IndexOutOfRangeException ex)
Console.WriteLine("Index is out of range: {0}", index);
throw;
Simplifique el código mediante la instrucción using de C#. Si tiene una instrucción
try-finally en la que el único código del bloque finally es una llamada al método
Dispose, use en su lugar una instrucción using .
En el ejemplo siguiente, la instrucción try - finally solo llama a Dispose en el
bloque finally .
C#
Font font1 = new Font("Arial", 10.0f);
try
byte charset = font1.GdiCharSet;
finally
if (font1 != null)
((IDisposable)font1).Dispose();
Puede hacer lo mismo con una instrucción using .
C#
using (Font font2 = new Font("Arial", 10.0f))
byte charset2 = font2.GdiCharSet;
Use la nueva sintaxis using que no requiere corchetes:
C#
using Font font3 = new Font("Arial", 10.0f);
byte charset3 = font3.GdiCharSet;
Operadores && y ||
Para evitar excepciones y aumentar el rendimiento omitiendo las comparaciones
innecesarias, use && en lugar de & y || en lugar de | cuando realice comparaciones,
como se muestra en el ejemplo siguiente.
C#
Console.Write("Enter a dividend: ");
int dividend = Convert.ToInt32(Console.ReadLine());
Console.Write("Enter a divisor: ");
int divisor = Convert.ToInt32(Console.ReadLine());
if ((divisor != 0) && (dividend / divisor > 0))
Console.WriteLine("Quotient: {0}", dividend / divisor);
else
Console.WriteLine("Attempted division by 0 ends up here.");
Si el divisor es 0, la segunda cláusula de la instrucción if produciría un error en tiempo
de ejecución. Pero el operador && se cortocircuita cuando la primera expresión es falsa.
Es decir, no evalúa la segunda expresión. El operador & evaluaría ambas, lo que
provocaría un error en tiempo de ejecución cuando divisor es 0.
Operador new
Use una de las formas concisas de creación de instancias de objeto, tal como se
muestra en las declaraciones siguientes. En el segundo ejemplo se muestra la
sintaxis que está disponible a partir de C# 9.
C#
var instance1 = new ExampleClass();
C#
ExampleClass instance2 = new();
Las declaraciones anteriores son equivalentes a la siguiente declaración.
C#
ExampleClass instance2 = new ExampleClass();
Use inicializadores de objeto para simplificar la creación de objetos, tal y como se
muestra en el ejemplo siguiente.
C#
var instance3 = new ExampleClass { Name = "Desktop", ID = 37414,
Location = "Redmond", Age = 2.3 };
En el ejemplo siguiente se establecen las mismas propiedades que en el ejemplo
anterior, pero no se utilizan inicializadores.
C#
var instance4 = new ExampleClass();
instance4.Name = "Desktop";
instance4.ID = 37414;
instance4.Location = "Redmond";
instance4.Age = 2.3;
Control de eventos
Si va a definir un controlador de eventos que no es necesario quitar más tarde, utilice
una expresión lambda.
C#
public Form2()
this.Click += (s, e) =>
MessageBox.Show(
((MouseEventArgs)e).Location.ToString());
};
La expresión lambda acorta la siguiente definición tradicional.
C#
public Form1()
this.Click += new EventHandler(Form1_Click);
void Form1_Click(object? sender, EventArgs e)
MessageBox.Show(((MouseEventArgs)e).Location.ToString());
Miembros estáticos
Llame a miembros estáticos con el nombre de clase: ClassName.StaticMember. Esta
práctica hace que el código sea más legible al clarificar el acceso estático. No califique
un miembro estático definido en una clase base con el nombre de una clase derivada.
Mientras el código se compila, su legibilidad se presta a confusión, y puede
interrumpirse en el futuro si se agrega a un miembro estático con el mismo nombre a la
clase derivada.
Consultas LINQ
Utilice nombres descriptivos para las variables de consulta. En el ejemplo siguiente,
se utiliza seattleCustomers para los clientes que se encuentran en Seattle.
C#
var seattleCustomers = from customer in customers
where customer.City == "Seattle"
select customer.Name;
Utilice alias para asegurarse de que los nombres de propiedad de tipos anónimos
se escriben correctamente con mayúscula o minúscula, usando para ello la grafía
Pascal.
C#
var localDistributors =
from customer in customers
join distributor in distributors on customer.City equals
distributor.City
select new { Customer = customer, Distributor = distributor };
Cambie el nombre de las propiedades cuando puedan ser ambiguos en el
resultado. Por ejemplo, si la consulta devuelve un nombre de cliente y un
identificador de distribuidor, en lugar de dejarlos como Name e ID en el resultado,
cambie su nombre para aclarar que Name es el nombre de un cliente e ID es el
identificador de un distribuidor.
C#
var localDistributors2 =
from customer in customers
join distributor in distributors on customer.City equals
distributor.City
select new { CustomerName = customer.Name, DistributorID =
distributor.ID };
Utilice tipos implícitos en la declaración de variables de consulta y variables de
intervalo.
C#
var seattleCustomers = from customer in customers
where customer.City == "Seattle"
select customer.Name;
Alinee las cláusulas de consulta bajo la cláusula from, como se muestra en los
ejemplos anteriores.
Use cláusulas where antes de otras cláusulas de consulta para asegurarse de que
las cláusulas de consulta posteriores operan en un conjunto de datos reducido y
filtrado.
C#
var seattleCustomers2 = from customer in customers
where customer.City == "Seattle"
orderby customer.Name
select customer;
Use varias cláusulas from en lugar de una cláusula join para obtener acceso a
colecciones internas. Por ejemplo, una colección de objetos Student podría
contener cada uno un conjunto de resultados de exámenes. Cuando se ejecuta la
siguiente consulta, devuelve cada resultado superior a 90, además del apellido del
alumno que recibió la puntuación.
C#
var scoreQuery = from student in students
from score in student.Scores!
where score > 90
select new { Last = student.LastName, score };
Seguridad
Siga las instrucciones de Instrucciones de codificación segura.
Consulte también
Instrucciones de codificación en el entorno de ejecución de .NET
Convenciones de código de Visual Basic
Instrucciones de codificación segura
Procedimiento para mostrar
argumentos de la línea de comandos
Artículo • 28/11/2022 • Tiempo de lectura: 2 minutos
Los argumentos proporcionados para un archivo ejecutable en la línea de comandos
son accesibles en instrucciones de nivel superior o mediante un parámetro opcional
para Main . Los argumentos se proporcionan en forma de una matriz de cadenas. Cada
elemento de la matriz contiene un argumento. Se quita el espacio en blanco entre los
argumentos. Por ejemplo, considere estas invocaciones de línea de comandos de un
ejecutable ficticio:
Entrada en la línea de comandos Matriz de cadenas que se pasa a Main
executable.exe a b c "a"
"b"
"c"
executable.exe one two "one"
"two"
executable.exe "one two" three "one two"
"three"
7 Nota
Si se ejecuta una aplicación en Visual Studio, se pueden especificar argumentos de
línea de comandos en la Página Depuración, Diseñador de proyectos.
Ejemplo
En este ejemplo se muestran los argumentos de la línea de comandos pasados a una
aplicación de la línea de comandos. La salida que se muestra corresponde a la primera
entrada de la tabla anterior.
C#
// The Length property provides the number of array elements.
Console.WriteLine($"parameter count = {args.Length}");
for (int i = 0; i < args.Length; i++)
Console.WriteLine($"Arg[{i}] = [{args[i]}]");
/* Output (assumes 3 cmd line args):
parameter count = 3
Arg[0] = [a]
Arg[1] = [b]
Arg[2] = [c]
*/
Consulte también
Introducción a System.CommandLine
Tutorial: Introducción a System.CommandLine
Exploración de la programación
orientada a objetos con clases y objetos
Artículo • 28/11/2022 • Tiempo de lectura: 10 minutos
En este tutorial, creará una aplicación de consola y conocerá las características básicas
orientadas a objetos que forman parte del lenguaje C#.
Requisitos previos
Se recomienda tener Visual Studio para Windows o Mac. Puede descargar una
versión gratuita desde la página de descargas de Visual Studio . Visual Studio
incluye el SDK de .NET.
También se puede usar el editor de Visual Studio Code . Deberá instalar el SDK
de .NET más reciente por separado.
Si prefiere usar otro editor, deberá instalar el SDK de .NET más reciente.
Creación de una aplicación
En una ventana de terminal, cree un directorio denominado clases. Creará la aplicación
ahí. Cambie a ese directorio y escriba dotnet new console en la ventana de la consola.
Este comando crea la aplicación. Abra Program.cs. El resultado debería tener un aspecto
similar a este:
C#
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");
En este tutorial, se van a crear tipos nuevos que representan una cuenta bancaria.
Normalmente los desarrolladores definen cada clase en un archivo de texto diferente.
De esta forma, la tarea de administración resulta más sencilla a medida que aumenta el
tamaño del programa. Cree un archivo denominado BankAccount.cs en el directorio
Classes.
Este archivo va a contener la definición de una cuenta bancaria. La programación
orientada a objetos organiza el código creando tipos en forma de clases. Estas clases
contienen el código que representa una entidad específica. La clase BankAccount
representa una cuenta bancaria. El código implementa operaciones específicas a través
de métodos y propiedades. En este tutorial, la cuenta bancaria admite el siguiente
comportamiento:
1. Tiene un número de diez dígitos que identifica la cuenta bancaria de forma única.
2. Tiene una cadena que almacena el nombre o los nombres de los propietarios.
3. Se puede consultar el saldo.
4. Acepta depósitos.
5. Acepta reintegros.
6. El saldo inicial debe ser positivo.
7. Los reintegros no pueden generar un saldo negativo.
Definición del tipo de cuenta bancaria
Puede empezar por crear los datos básicos de una clase que define dicho
comportamiento. Cree un archivo con el comando File:New. Asígnele el nombre
BankAccount.cs. Agregue el código siguiente al archivo BankAccount.cs:
C#
namespace Classes;
public class BankAccount
public string Number { get; }
public string Owner { get; set; }
public decimal Balance { get; }
public void MakeDeposit(decimal amount, DateTime date, string note)
public void MakeWithdrawal(decimal amount, DateTime date, string note)
Antes de avanzar, se va a dar un repaso a lo que ha compilado. La declaración
namespace permite organizar el código de forma lógica. Este tutorial es relativamente
pequeño, por lo que deberá colocar todo el código en un espacio de nombres.
public class BankAccount define la clase o el tipo que quiere crear. Todo lo que se
encuentra entre { y } después de la declaración de clase define el estado y el
comportamiento de la clase. La clase BankAccount cuenta con cinco miembros. Los tres
primeros son propiedades. Las propiedades son elementos de datos que pueden
contener código que exige la validación u otras reglas. Los dos últimos son métodos.
Los métodos son bloques de código que realizan una única función. La lectura de los
nombres de cada miembro debe proporcionar suficiente información tanto al usuario
como a otro desarrollador para entender cuál es la función de la clase.
Apertura de una cuenta nueva
La primera característica que se va a implementar es la apertura de una cuenta bancaria.
Cuando un cliente abre una cuenta, debe proporcionar un saldo inicial y la información
sobre el propietario o los propietarios de esa cuenta.
Para crear un objeto de tipo BankAccount , es necesario definir un constructor que asigne
esos valores. Un constructor es un miembro que tiene el mismo nombre que la clase. Se
usa para inicializar los objetos de ese tipo de clase. Agregue el siguiente constructor al
tipo BankAccount . Coloque el siguiente código encima de la declaración de MakeDeposit .
C#
public BankAccount(string name, decimal initialBalance)
this.Owner = name;
this.Balance = initialBalance;
En el código anterior se identifican las propiedades del objeto que se está construyendo
mediante la inclusión del calificador this . Ese calificador suele ser opcional y se omite.
También podría haber escrito lo siguiente:
C#
public BankAccount(string name, decimal initialBalance)
Owner = name;
Balance = initialBalance;
El calificador this solo es necesario cuando una variable o un parámetro local tiene el
mismo nombre que el campo o la propiedad. El calificador this se omite en el resto de
este artículo, a menos que sea necesario.
A los constructores se les llama cuando se crea un objeto mediante new. Reemplace la
línea Console.WriteLine("Hello World!"); de Program.cs por la siguiente línea
(reemplace <name> por su nombre):
C#
using Classes;
var account = new BankAccount("<name>", 1000);
Console.WriteLine($"Account {account.Number} was created for {account.Owner}
with {account.Balance} initial balance.");
Vamos a ejecutar lo que se ha creado hasta ahora. Si usa Visual Studio, seleccione Iniciar
sin depurar en el menú Depurar. Si va a usar una línea de comandos, escriba dotnet
run en el directorio en el que ha creado el proyecto.
¿Ha observado que el número de cuenta está en blanco? Es el momento de
solucionarlo. El número de cuenta debe asignarse cuando se construye el objeto. Sin
embargo, el autor de la llamada no es el responsable de crearlo. El código de la clase
BankAccount debe saber cómo asignar nuevos números de cuenta. Una manera sencilla
de empezar es con un número de diez dígitos. Increméntelo cuando cree cada cuenta.
Por último, almacene el número de cuenta actual cuando se construya un objeto.
Agregue una declaración de miembro a la clase BankAccount . Coloque la siguiente línea
de código después de la llave de apertura { al principio de la clase BankAccount :
C#
private static int accountNumberSeed = 1234567890;
accountNumberSeed es un miembro de datos. Tiene el estado private , lo que significa
que solo se puede acceder a él con el código incluido en la clase BankAccount . Es una
forma de separar las responsabilidades públicas (como tener un número de cuenta) de
la implementación privada (cómo se generan los números de cuenta). También es
static , lo que significa que lo comparten todos los objetos BankAccount . El valor de
una variable no estática es único para cada instancia del objeto BankAccount . Agregue
las dos líneas siguientes al constructor para asignar el número de cuenta: Colóquelas
después de la línea donde pone this.Balance = initialBalance :
C#
this.Number = accountNumberSeed.ToString();
accountNumberSeed++;
Escriba dotnet run para ver los resultados.
Creación de depósitos y reintegros
La clase de la cuenta bancaria debe aceptar depósitos y reintegros para que el
funcionamiento sea adecuado. Se van a implementar depósitos y reintegros con la
creación de un diario de cada transacción de la cuenta. Hacer un seguimiento de cada
transacción ofrece algunas ventajas con respecto a limitarse a actualizar el saldo en cada
transacción. El historial se puede utilizar para auditar todas las transacciones y
administrar los saldos diarios. Con el cálculo del saldo a partir del historial de todas las
transacciones, cuando proceda, nos aseguramos de que todos los errores de una única
transacción que se solucionen se reflejarán correctamente en el saldo cuando se haga el
siguiente cálculo.
Se va a empezar por crear un tipo para representar una transacción. La transacción es un
tipo simple que no tiene ninguna responsabilidad. Necesita algunas propiedades. Cree
un archivo denominado Transaction.cs. Agregue el código siguiente a él:
C#
namespace Classes;
public class Transaction
public decimal Amount { get; }
public DateTime Date { get; }
public string Notes { get; }
public Transaction(decimal amount, DateTime date, string note)
Amount = amount;
Date = date;
Notes = note;
Ahora se va a agregar List<T> de objetos Transaction a la clase BankAccount . Agregue
la siguiente declaración después del constructor en el archivo BankAccount.cs:
C#
private List<Transaction> allTransactions = new List<Transaction>();
Ahora vamos a calcular correctamente Balance . El saldo actual se puede averiguar si se
suman los valores de todas las transacciones. Como el código es actual, solo puede
obtener el saldo inicial de la cuenta, así que tiene que actualizar la propiedad Balance .
Reemplace la línea public decimal Balance { get; } de BankAccount.cs con el código
siguiente:
C#
public decimal Balance
get
decimal balance = 0;
foreach (var item in allTransactions)
balance += item.Amount;
return balance;
En este ejemplo se muestra un aspecto importante de las propiedades. Ahora va a
calcular el saldo cuando otro programador solicite el valor. El cálculo enumera todas las
transacciones y proporciona la suma como el saldo actual.
Después, implemente los métodos MakeDeposit y MakeWithdrawal . Estos métodos
aplicarán las dos reglas finales: el saldo inicial debe ser positivo, y ningún reintegro debe
generar un saldo negativo.
Las reglas presentan el concepto de las excepciones. La forma habitual de indicar que un
método no puede completar su trabajo correctamente consiste en generar una
excepción. El tipo de excepción y el mensaje asociado a ella describen el error. En este
caso, el método MakeDeposit genera una excepción si el importe del depósito no es
mayor que 0. El método MakeWithdrawal genera una excepción si el importe del
reintegro no es mayor que 0 o si el procesamiento de la operación tiene como resultado
un saldo negativo: Agregue el código siguiente después de la declaración de la lista
allTransactions :
C#
public void MakeDeposit(decimal amount, DateTime date, string note)
if (amount <= 0)
throw new ArgumentOutOfRangeException(nameof(amount), "Amount of
deposit must be positive");
var deposit = new Transaction(amount, date, note);
allTransactions.Add(deposit);
}
public void MakeWithdrawal(decimal amount, DateTime date, string note)
if (amount <= 0)
throw new ArgumentOutOfRangeException(nameof(amount), "Amount of
withdrawal must be positive");
if (Balance - amount < 0)
throw new InvalidOperationException("Not sufficient funds for this
withdrawal");
var withdrawal = new Transaction(-amount, date, note);
allTransactions.Add(withdrawal);
La instrucción throwgenera una excepción. La ejecución del bloque actual finaliza y el
control se transfiere al primer bloque catch coincidente que se encuentra en la pila de
llamadas. Se agregará un bloque catch para probar este código un poco más adelante.
El constructor debe obtener un cambio para que agregue una transacción inicial, en
lugar de actualizar el saldo directamente. Puesto que ya escribió el método
MakeDeposit , llámelo desde el constructor. El constructor terminado debe tener este
aspecto:
C#
public BankAccount(string name, decimal initialBalance)
Number = accountNumberSeed.ToString();
accountNumberSeed++;
Owner = name;
MakeDeposit(initialBalance, DateTime.Now, "Initial balance");
DateTime.Now es una propiedad que devuelve la fecha y hora actuales. Para probar este
código, agregue algunos depósitos y reintegros en el método Main , siguiendo el código
con el que se crea un elemento BankAccount :
C#
account.MakeWithdrawal(500, DateTime.Now, "Rent payment");
Console.WriteLine(account.Balance);
account.MakeDeposit(100, DateTime.Now, "Friend paid me back");
Console.WriteLine(account.Balance);
Después, compruebe si detecta las condiciones de error intentando crear una cuenta
con un saldo negativo. Agregue el código siguiente después del código anterior que
acaba de agregar:
C#
// Test that the initial balances must be positive.
BankAccount invalidAccount;
try
invalidAccount = new BankAccount("invalid", -55);
catch (ArgumentOutOfRangeException e)
Console.WriteLine("Exception caught creating account with negative
balance");
Console.WriteLine(e.ToString());
return;
Use las instrucciones try y catch para marcar un bloque de código que puede generar
excepciones y para detectar los errores que se esperan. Puede usar la misma técnica
para probar el código que genera una excepción para un saldo negativo. Agregue el
código siguiente antes de la declaración de invalidAccount en el método Main :
C#
// Test for a negative balance.
try
account.MakeWithdrawal(750, DateTime.Now, "Attempt to overdraw");
catch (InvalidOperationException e)
Console.WriteLine("Exception caught trying to overdraw");
Console.WriteLine(e.ToString());
Guarde el archivo y escriba dotnet run para probarlo.
Desafío: registro de todas las transacciones
Para finalizar este tutorial, puede escribir el método GetAccountHistory que crea string
para el historial de transacciones. Agregue este método al tipo BankAccount :
C#
public string GetAccountHistory()
var report = new System.Text.StringBuilder();
decimal balance = 0;
report.AppendLine("Date\t\tAmount\tBalance\tNote");
foreach (var item in allTransactions)
balance += item.Amount;
report.AppendLine($"
{item.Date.ToShortDateString()}\t{item.Amount}\t{balance}\t{item.Notes}");
return report.ToString();
En el historial se usa la clase StringBuilder para dar formato a una cadena que contiene
una línea para cada transacción. Se ha visto anteriormente en estos tutoriales el código
utilizado para dar formato a una cadena. Un carácter nuevo es \t . Inserta una pestaña
para dar formato a la salida.
Agregue esta línea para probarla en Program.cs:
C#
Console.WriteLine(account.GetAccountHistory());
Ejecute el programa para ver los resultados.
Pasos siguientes
Si se ha quedado bloqueado, puede consultar el origen de este tutorial en el repositorio
de GitHub .
Puede continuar con el tutorial de la programación orientada a objetos.
Puede aprender más sobre estos conceptos en los artículos siguientes:
Instrucciones de selección
Instrucciones de iteración
Programación orientada a objetos (C#)
Artículo • 28/11/2022 • Tiempo de lectura: 12 minutos
C# es un lenguaje de programación orientado a objetos. Los cuatro principios básicos
de la programación orientada a objetos son:
Abstracción: modelar los atributos e interacciones pertinentes de las entidades
como clases para definir una representación abstracta de un sistema.
Encapsulación: ocultar el estado interno y la funcionalidad de un objeto y permitir
solo el acceso a través de un conjunto público de funciones.
Herencia: capacidad de crear nuevas abstracciones basadas en abstracciones
existentes.
Polimorfismo: capacidad de implementar propiedades o métodos heredados de
maneras diferentes en varias abstracciones.
En el tutorial anterior, Introducción a las clases se trató la abstracción y la encapsulación.
La clase BankAccount proporcionó una abstracción para el concepto de una cuenta
bancaria. Puede modificar su implementación sin que afecte para nada al código que
usó la clase BankAccount . Las clases BankAccount y Transaction proporcionan
encapsulación de los componentes necesarios para describir esos conceptos en el
código.
En este tutorial, ampliará la aplicación para hacer uso de la herencia y el polimorfismo
para agregar nuevas características. También agregará características a la clase
BankAccount , aprovechando las técnicas de abstracción y encapsulación que aprendió en
el tutorial anterior.
Creación de diferentes tipos de cuentas
Después de compilar este programa, recibirá solicitudes para agregarle características.
Funciona bien en situaciones en las que solo hay un tipo de cuenta bancaria. Con el
tiempo, las necesidades cambian y se solicitan tipos de cuenta relacionados:
Una cuenta que devenga intereses que genera beneficios al final de cada mes.
Una línea de crédito que puede tener un saldo negativo, pero cuando sea así, se
producirá un cargo por intereses cada mes.
Una cuenta de tarjeta de regalo de prepago que comienza con un único depósito
y solo se puede liquidar. Se puede recargar una vez al principio de cada mes.
Todas estas cuentas diferentes son similares a la clase BankAccount definida en el tutorial
anterior. Podría copiar ese código, cambiar el nombre de las clases y realizar
modificaciones. Esa técnica funcionaría a corto plazo, pero a la larga supondría más
trabajo. Cualquier cambio se copiará en todas las clases afectadas.
En su lugar, puede crear nuevos tipos de cuenta bancaria que hereden métodos y datos
de la clase BankAccount creada en el tutorial anterior. Estas clases nuevas pueden
extender la clase BankAccount con el comportamiento específico necesario para cada
tipo:
C#
public class InterestEarningAccount : BankAccount
public class LineOfCreditAccount : BankAccount
{
public class GiftCardAccount : BankAccount
Cada una de estas clases hereda el comportamiento compartido de su clase base
compartida, la clase BankAccount . Escriba las implementaciones para la funcionalidad
nueva y diferente en cada una de las clases derivadas. Estas clases derivadas ya tienen
todo el comportamiento definido en la clase BankAccount .
Es recomendable crear cada clase nueva en un archivo de código fuente diferente. En
Visual Studio , puede hacer clic con el botón derecho en el proyecto y seleccionar
Agregar clase para agregar una clase nueva en un archivo nuevo. En Visual Studio
Code , seleccione Archivo y luego Nuevo para crear un nuevo archivo de código
fuente. En cualquier herramienta, ponga un nombre al archivo que coincida con la clase:
InterestEarningAccount.cs, LineOfCreditAccount.cs y GiftCardAccount.cs.
Cuando cree las clases como se muestra en el ejemplo anterior, observará que ninguna
de las clases derivadas se compila. La inicialización de un objeto es responsabilidad de
un constructor. Un constructor de clase derivada debe inicializar la clase derivada y
proporcionar instrucciones sobre cómo inicializar el objeto de la clase base incluido en
la clase derivada. Normalmente, se produce una inicialización correcta sin ningún
código adicional. La clase BankAccount declara un constructor público con la siguiente
firma:
C#
public BankAccount(string name, decimal initialBalance)
El compilador no genera un constructor predeterminado al definir un constructor. Esto
significa que cada clase derivada debe llamar explícitamente a este constructor. Se
declara un constructor que puede pasar argumentos al constructor de la clase base. En
el código siguiente se muestra el constructor de InterestEarningAccount :
C#
public InterestEarningAccount(string name, decimal initialBalance) :
base(name, initialBalance)
Los parámetros de este nuevo constructor coinciden con el tipo de parámetro y los
nombres del constructor de clase base. Utilice la sintaxis de : base() para indicar una
llamada a un constructor de clase base. Algunas clases definen varios constructores, y
esta sintaxis le permite elegir el constructor de clase base al que llama. Una vez que
haya actualizado los constructores, puede desarrollar el código para cada una de las
clases derivadas. Los requisitos para las clases nuevas se pueden indicar de la siguiente
manera:
Una cuenta que devenga intereses:
obtendrá un crédito del 2 % del saldo a final de mes.
Una línea de crédito:
puede tener un saldo negativo, pero no mayor en valor absoluto que el límite
de crédito.
Generará un cargo por intereses cada mes en el que el saldo final del mes no
sea 0.
Generará un cargo por cada retirada que supere el límite de crédito.
Una cuenta de tarjeta regalo:
Se puede recargar con una cantidad especificada una vez al mes, el último día
del mes.
Puede ver que los tres tipos de cuenta tienen una acción que tiene lugar al final de cada
mes. Sin embargo, cada tipo de cuenta realiza diferentes tareas. Utiliza el polimorfismo
para implementar este código. Cree un método virtual único en la clase BankAccount :
C#
public virtual void PerformMonthEndTransactions() { }
El código anterior muestra cómo se usa la palabra clave virtual para declarar un
método en la clase base para el que una clase derivada puede proporcionar una
implementación diferente. Un método virtual es un método en el que cualquier clase
derivada puede optar por volver a implementar. Las clases derivadas usan la palabra
clave override para definir la nueva implementación. Normalmente, se hace referencia a
este proceso como "reemplazar la implementación de la clase base". La palabra clave
virtual especifica que las clases derivadas pueden invalidar el comportamiento.
También puede declarar métodos abstract en los que las clases derivadas deben
reemplazar el comportamiento. La clase base no proporciona una implementación para
un método abstract . A continuación, debe definir la implementación de dos de las
nuevas clases que ha creado. Empiece por InterestEarningAccount :
C#
public override void PerformMonthEndTransactions()
if (Balance > 500m)
{
decimal interest = Balance * 0.05m;
MakeDeposit(interest, DateTime.Now, "apply monthly interest");
Agregue el código siguiente a LineOfCreditAccount . El código niega el saldo para
calcular un cargo de interés positivo que se retira de la cuenta:
C#
public override void PerformMonthEndTransactions()
if (Balance < 0)
// Negate the balance to get a positive interest charge:
decimal interest = -Balance * 0.07m;
MakeWithdrawal(interest, DateTime.Now, "Charge monthly interest");
La clase GiftCardAccount necesita dos cambios para implementar su funcionalidad de
fin de mes. En primer lugar, modifique el constructor para incluir una cantidad opcional
para agregar cada mes:
C#
private readonly decimal _monthlyDeposit = 0m;
public GiftCardAccount(string name, decimal initialBalance, decimal
monthlyDeposit = 0) : base(name, initialBalance)
=> _monthlyDeposit = monthlyDeposit;
El constructor proporciona un valor predeterminado para el valor monthlyDeposit , por lo
que los llamadores pueden omitir 0 para ningún ingreso mensual. A continuación,
invalide el método PerformMonthEndTransactions para agregar el depósito mensual, si se
estableció en un valor distinto de cero en el constructor:
C#
public override void PerformMonthEndTransactions()
if (_monthlyDeposit != 0)
MakeDeposit(_monthlyDeposit, DateTime.Now, "Add monthly deposit");
La invalidación aplica el conjunto de depósitos mensual en el constructor. Agregue el
código siguiente al método Main para probar estos cambios en GiftCardAccount y en
InterestEarningAccount :
C#
var giftCard = new GiftCardAccount("gift card", 100, 50);
giftCard.MakeWithdrawal(20, DateTime.Now, "get expensive coffee");
giftCard.MakeWithdrawal(50, DateTime.Now, "buy groceries");
giftCard.PerformMonthEndTransactions();
// can make additional deposits:
giftCard.MakeDeposit(27.50m, DateTime.Now, "add some additional spending
money");
Console.WriteLine(giftCard.GetAccountHistory());
var savings = new InterestEarningAccount("savings account", 10000);
savings.MakeDeposit(750, DateTime.Now, "save some money");
savings.MakeDeposit(1250, DateTime.Now, "Add more savings");
savings.MakeWithdrawal(250, DateTime.Now, "Needed to pay monthly bills");
savings.PerformMonthEndTransactions();
Console.WriteLine(savings.GetAccountHistory());
Verifique los resultados. Ahora, agregue un conjunto similar de código de prueba para
LineOfCreditAccount :
var lineOfCredit = new LineOfCreditAccount("line of credit", 0);
// How much is too much to borrow?
lineOfCredit.MakeWithdrawal(1000m, DateTime.Now, "Take out monthly
advance");
lineOfCredit.MakeDeposit(50m, DateTime.Now, "Pay back small amount");
lineOfCredit.MakeWithdrawal(5000m, DateTime.Now, "Emergency funds for
repairs");
lineOfCredit.MakeDeposit(150m, DateTime.Now, "Partial restoration on
repairs");
lineOfCredit.PerformMonthEndTransactions();
Console.WriteLine(lineOfCredit.GetAccountHistory());
Al agregar el código anterior y ejecutar el programa, verá algo parecido al siguiente
error:
Consola
Unhandled exception. System.ArgumentOutOfRangeException: Amount of deposit
must be positive (Parameter 'amount')
at OOProgramming.BankAccount.MakeDeposit(Decimal amount, DateTime date,
String note) in BankAccount.cs:line 42
at OOProgramming.BankAccount..ctor(String name, Decimal initialBalance)
in BankAccount.cs:line 31
at OOProgramming.LineOfCreditAccount..ctor(String name, Decimal
initialBalance) in LineOfCreditAccount.cs:line 9
at OOProgramming.Program.Main(String[] args) in Program.cs:line 29
7 Nota
La salida real incluye la ruta de acceso completa a la carpeta con el proyecto. Los
nombres de carpeta se omitieron para ser más breves. Además, dependiendo del
formato del código, los números de línea pueden ser ligeramente diferentes.
Este código produce un error porque BankAccount supone que el saldo inicial debe ser
mayor que 0. Otra suposición incorporada en la clase BankAccount es que el saldo no
puede entrar en cifras negativas. Lo que sucede que es se rechazan las retiradas que
provocan un descubierto en la cuenta. Ambas suposiciones deben cambiar. La línea de
la cuenta de crédito comienza en 0, y generalmente tendrá un saldo negativo. Además,
si un cliente retira demasiado dinero, generará un cargo. La transacción se acepta, solo
que cuesta más. La primera regla se puede implementar agregando un argumento
opcional al constructor BankAccount que especifica el saldo mínimo. El valor
predeterminado es 0 . La segunda regla requiere un mecanismo que permita que las
clases derivadas modifiquen el algoritmo predeterminado. En cierto sentido, la clase
base "pregunta" al tipo derivado qué debe ocurrir cuando hay un descubierto. El
comportamiento predeterminado es rechazar la transacción generando una excepción.
Comencemos agregando un segundo constructor que incluya un parámetro
minimumBalance opcional. Este nuevo constructor se ocupa de todas las acciones que
realiza el constructor existente. Además, establece la propiedad del saldo mínimo.
Puede copiar el cuerpo del constructor existente, pero eso significa que dos ubicaciones
cambiarán en el futuro. Lo que puede hacer es usar un encadenamiento de constructores
para que un constructor llame a otro. En el código siguiente se muestran los dos
constructores y el nuevo campo adicional:
C#
private readonly decimal _minimumBalance;
public BankAccount(string name, decimal initialBalance) : this(name,
initialBalance, 0) { }
public BankAccount(string name, decimal initialBalance, decimal
minimumBalance)
Number = s_accountNumberSeed.ToString();
s_accountNumberSeed++;
Owner = name;
_minimumBalance = minimumBalance;
if (initialBalance > 0)
MakeDeposit(initialBalance, DateTime.Now, "Initial balance");
En el código anterior se muestran dos técnicas nuevas. En primer lugar, el campo
minimumBalance está marcado como readonly . Esto significa que el valor no se puede
cambiar después de que se construya el objeto. Una vez que se crea BankAccount ,
minimumBalance no puede cambiar. En segundo lugar, el constructor que toma dos
parámetros utiliza : this(name, initialBalance, 0) { } como su implementación. La
expresión : this() llama al otro constructor, el que tiene tres parámetros. Esta técnica
permite tener una única implementación para inicializar un objeto, aunque el código de
cliente puede elegir uno de muchos constructores.
Esta implementación solo llama a MakeDeposit si el saldo inicial es mayor que 0 . Esto
conserva la regla de que los depósitos deben ser positivos, pero permite que la cuenta
de crédito se abra con un saldo de 0 .
Ahora que la clase BankAccount tiene un campo de solo lectura para el saldo mínimo, el
último cambio es modificar la codificación rígida 0 a minimumBalance en el método
MakeWithdrawal :
C#
if (Balance - amount < minimumBalance)
Después de extender la clase BankAccount , puede modificar el constructor
LineOfCreditAccount para llamar al nuevo constructor base, como se muestra en el
código siguiente:
C#
public LineOfCreditAccount(string name, decimal initialBalance, decimal
creditLimit) : base(name, initialBalance, -creditLimit)
Observe que el constructor LineOfCreditAccount cambia el signo del parámetro
creditLimit para que coincida con el significado del parámetro minimumBalance .
Diferentes reglas de descubierto
La última característica que se va a agregar permite a LineOfCreditAccount cobrar una
cuota por sobrepasar el límite de crédito en lugar de rechazar la transacción.
Una técnica consiste en definir una función virtual en la que se implemente el
comportamiento requerido. La clase BankAccount refactoriza el método MakeWithdrawal
en dos métodos. El nuevo método realiza la acción especificada cuando la retirada toma
el saldo por debajo del mínimo. El método MakeWithdrawal existente tiene el siguiente
código:
C#
public void MakeWithdrawal(decimal amount, DateTime date, string note)
if (amount <= 0)
throw new ArgumentOutOfRangeException(nameof(amount), "Amount of
withdrawal must be positive");
if (Balance - amount < _minimumBalance)
throw new InvalidOperationException("Not sufficient funds for this
withdrawal");
var withdrawal = new Transaction(-amount, date, note);
allTransactions.Add(withdrawal);
Reemplácelo por el código siguiente:
C#
public void MakeWithdrawal(decimal amount, DateTime date, string note)
if (amount <= 0)
throw new ArgumentOutOfRangeException(nameof(amount), "Amount of
withdrawal must be positive");
Transaction? overdraftTransaction = CheckWithdrawalLimit(Balance -
amount < _minimumBalance);
Transaction? withdrawal = new(-amount, date, note);
_allTransactions.Add(withdrawal);
if (overdraftTransaction != null)
_allTransactions.Add(overdraftTransaction);
protected virtual Transaction? CheckWithdrawalLimit(bool isOverdrawn)
if (isOverdrawn)
throw new InvalidOperationException("Not sufficient funds for this
withdrawal");
else
return default;
}
El método agregado es protected , lo que significa que solo se puede llamar desde
clases derivadas. Esa declaración impide que otros clientes llamen al método. También
es virtual para que las clases derivadas puedan cambiar el comportamiento. El tipo de
valor devuelto es Transaction? . La anotación ? indica que el método puede devolver
null . Agregue la siguiente implementación en LineOfCreditAccount para cobrar una
cuota cuando se supere el límite de retirada:
C#
protected override Transaction? CheckWithdrawalLimit(bool isOverdrawn) =>
isOverdrawn
? new Transaction(-20, DateTime.Now, "Apply overdraft fee")
: default;
El reemplazo devuelve una transacción de cuota cuando en la cuenta se produce un
descubierto. Si la retirada no supera el límite, el método devuelve una transacción null .
Esto indica que no hay ninguna cuota. Pruebe estos cambios agregando el código
siguiente al método Main en la clase Program :
C#
var lineOfCredit = new LineOfCreditAccount("line of credit", 0, 2000);
// How much is too much to borrow?
lineOfCredit.MakeWithdrawal(1000m, DateTime.Now, "Take out monthly
advance");
lineOfCredit.MakeDeposit(50m, DateTime.Now, "Pay back small amount");
lineOfCredit.MakeWithdrawal(5000m, DateTime.Now, "Emergency funds for
repairs");
lineOfCredit.MakeDeposit(150m, DateTime.Now, "Partial restoration on
repairs");
lineOfCredit.PerformMonthEndTransactions();
Console.WriteLine(lineOfCredit.GetAccountHistory());
Ejecute el programa y compruebe los resultados.
Resumen
Si se ha quedado bloqueado, puede consultar el origen de este tutorial en el repositorio
de GitHub .
En este tutorial se han mostrado muchas de las técnicas que se usan en la programación
orientada a objetos:
Usó la abstracción cuando definió clases para cada uno de los distintos tipos de
cuenta. Esas clases describían el comportamiento de ese tipo de cuenta.
Usó la encapsulación cuando mantuvo muchos detalles private en cada clase.
Usó la herencia cuando aprovechó la implementación ya creada en la clase
BankAccount para guardar el código.
Usó el polimorfismo cuando creó métodos virtual que las clases derivadas
podrían reemplazar para crear un comportamiento específico para ese tipo de
cuenta.
Herencia en C# y .NET
Artículo • 04/02/2023 • Tiempo de lectura: 27 minutos
Este tutorial es una introducción a la herencia en C#. La herencia es una característica de
los lenguajes de programación orientados a objetos que permite definir una clase base,
que proporciona funcionalidad específica (datos y comportamiento), así como clases
derivadas, que heredan o invalidan esa funcionalidad.
Requisitos previos
Se recomienda tener Visual Studio para Windows o Mac. Puede descargar una
versión gratuita desde la página de descargas de Visual Studio . Visual Studio
incluye el SDK de .NET.
También se puede usar el editor de Visual Studio Code . Deberá instalar el SDK
de .NET más reciente por separado.
Si prefiere usar otro editor, deberá instalar el SDK de .NET más reciente.
Ejecución de los ejemplos
Para crear y ejecutar los ejemplos de este tutorial, use la utilidad dotnet desde la línea
de comandos. Siga estos pasos para cada ejemplo:
1. Cree un directorio para almacenar el ejemplo.
2. Escriba el comando dotnet new console en el símbolo del sistema para crear un
nuevo proyecto de .NET Core.
3. Copie y pegue el código del ejemplo en el editor de código.
4. Escriba el comando dotnet restore desde la línea de comandos para cargar o
restaurar las dependencias del 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.
5. Escriba el comando dotnet run para compilar y ejecutar el ejemplo.
Información previa: ¿Qué es la herencia?
La herencia es uno de los atributos fundamentales de la programación orientada a
objetos. Permite definir una clase secundaria que reutiliza (hereda), amplía o modifica el
comportamiento de una clase primaria. La clase cuyos miembros son heredados se
conoce como clase base. La clase que hereda los miembros de la clase base se conoce
como clase derivada.
C# y .NET solo admiten herencia única. Es decir, una clase solo puede heredar de una
clase única. Sin embargo, la herencia es transitiva, lo que le permite definir una jerarquía
de herencia para un conjunto de tipos. En otras palabras, el tipo D puede heredar del
tipo C , que hereda del tipo B , que hereda del tipo de clase base A . Dado que la
herencia es transitiva, los miembros de tipo A están disponibles para el tipo D .
No todos los miembros de una clase base los heredan las clases derivadas. Los
siguientes miembros no se heredan:
Constructores estáticos, que inicializan los datos estáticos de una clase.
Constructores de instancias, a los que se llama para crear una nueva instancia de la
clase. Cada clase debe definir sus propios constructores.
Finalizadores, llamados por el recolector de elementos no utilizados en tiempo de
ejecución para destruir instancias de una clase.
Si bien las clases derivadas heredan todos los demás miembros de una clase base, que
dichos miembros estén o no visibles depende de su accesibilidad. La accesibilidad del
miembro afecta a su visibilidad en las clases derivadas del modo siguiente:
Los miembros privados solo son visible en las clases derivadas que están anidadas
en su clase base. De lo contrario, no son visibles en las clases derivadas. En el
ejemplo siguiente, A.B es una clase anidada que se deriva de A , y C se deriva de
A . El campo privado A._value es visible en A.B. Pero si quita los comentarios del
método C.GetValue e intenta compilar el ejemplo, se produce el error del
compilador CS0122: "'A._value' no es accesible debido a su nivel de protección".
C#
public class A
private int _value = 10;
public class B : A
public int GetValue()
return _value;
public class C : A
// public int GetValue()
// {
// return _value;
// }
public class AccessExample
public static void Main(string[] args)
var b = new A.B();
Console.WriteLine(b.GetValue());
// The example displays the following output:
// 10
Los miembros protegidos solo son visibles en las clases derivadas.
Los miembros internos solo son visibles en las clases derivadas que se encuentran
en el mismo ensamblado que la clase base. No son visibles en las clases derivadas
ubicadas en un ensamblado diferente al de la clase base.
Los miembros públicos son visibles en las clases derivadas y forman parte de la
interfaz pública de dichas clases. Los miembros públicos heredados se pueden
llamar como si se definieran en la clase derivada. En el ejemplo siguiente, la clase
A define un método denominado Method1 y la clase B hereda de la clase A . El
ejemplo llama a Method1 como si fuera un método de instancia en B .
C#
public class A
public void Method1()
// Method implementation.
}
public class B : A
{ }
public class Example
public static void Main()
B b = new ();
b.Method1();
Las clases derivadas pueden también invalidar los miembros heredados al proporcionar
una implementación alternativa. Para poder invalidar un miembro, el miembro de la
clase base debe marcarse con la palabra clave virtual. De forma predeterminada, los
miembros de clase base no están marcados con virtual y no se pueden invalidar. Al
intentar invalidar un miembro no virtual, como se muestra en el ejemplo siguiente, se
genera el error del compilador CS0506: "<el miembro> no puede invalidar el miembro
<> heredado porque no está marcado como virtual, abstracto o invalidado".
C#
public class A
public void Method1()
// Do something.
public class B : A
public override void Method1() // Generates CS0506.
// Do something else.
En algunos casos, una clase derivada debe invalidar la implementación de la clase base.
Los miembros de clase base marcados con la palabra clave abstract requieren que las
clases derivadas los invaliden. Al intentar compilar el ejemplo siguiente, se genera el
error de compilador CS0534, "<class> no implementa el miembro abstracto heredado
<member>", porque la clase B no proporciona ninguna implementación para
A.Method1 .
C#
public abstract class A
public abstract void Method1();
public class B : A // Generates CS0534.
public void Method3()
// Do something.
La herencia solo se aplica a clases e interfaces. Other type categories (structs, delegates,
and enums) do not support inheritance. Debido a estas reglas, al intentar compilar
código como en el ejemplo siguiente se produce el error del compilador CS0527: "El
tipo 'ValueType' en la lista de interfaz no es una interfaz". El mensaje de error indica que,
aunque se pueden definir las interfaces que implementa una estructura, no se admite la
herencia.
C#
public struct ValueStructure : ValueType // Generates CS0527.
Herencia implícita
Aparte de los tipos de los que puedan heredar mediante herencia única, todos los tipos
del sistema de tipos .NET heredan implícitamente de Object o de un tipo derivado de
este. La funcionalidad común de Object está disponible para cualquier tipo.
Para ver lo que significa la herencia implícita, vamos a definir una nueva clase,
SimpleClass , que es simplemente una definición de clase vacía:
C#
public class SimpleClass
{ }
Después, se puede usar la reflexión (que permite inspeccionar los metadatos de un tipo
para obtener información sobre ese tipo) con el fin de obtener una lista de los
miembros que pertenecen al tipo SimpleClass . Aunque no se ha definido ningún
miembro en la clase SimpleClass , la salida del ejemplo indica que en realidad tiene
nueve miembros. Uno de ellos es un constructor sin parámetros (o predeterminado) que
el compilador de C# proporciona de manera automática para el tipo SimpleClass . Los
ocho restantes son miembros de Object, el tipo del que heredan implícitamente a la
larga todas las clases e interfaces del sistema de tipo .NET.
C#
using System.Reflection;
public class SimpleClassExample
public static void Main()
Type t = typeof(SimpleClass);
BindingFlags flags = BindingFlags.Instance | BindingFlags.Static |
BindingFlags.Public |
BindingFlags.NonPublic |
BindingFlags.FlattenHierarchy;
MemberInfo[] members = t.GetMembers(flags);
Console.WriteLine($"Type {t.Name} has {members.Length} members: ");
foreach (MemberInfo member in members)
string access = "";
string stat = "";
var method = member as MethodBase;
if (method != null)
if (method.IsPublic)
access = " Public";
else if (method.IsPrivate)
access = " Private";
else if (method.IsFamily)
access = " Protected";
else if (method.IsAssembly)
access = " Internal";
else if (method.IsFamilyOrAssembly)
access = " Protected Internal ";
if (method.IsStatic)
stat = " Static";
string output = $"{member.Name} ({member.MemberType}): {access}
{stat}, Declared by {member.DeclaringType}";
Console.WriteLine(output);
// The example displays the following output:
// Type SimpleClass has 9 members:
// ToString (Method): Public, Declared by System.Object
// Equals (Method): Public, Declared by System.Object
// Equals (Method): Public Static, Declared by System.Object
// ReferenceEquals (Method): Public Static, Declared by System.Object
// GetHashCode (Method): Public, Declared by System.Object
// GetType (Method): Public, Declared by System.Object
// Finalize (Method): Internal, Declared by System.Object
// MemberwiseClone (Method): Internal, Declared by System.Object
// .ctor (Constructor): Public, Declared by SimpleClass
La herencia implícita desde la clase Object permite que estos métodos estén disponibles
para la clase SimpleClass :
El método público ToString , que convierte un objeto SimpleClass en su
representación de cadena, devuelve el nombre de tipo completo. En este caso, el
método ToString devuelve la cadena "SimpleClass".
Tres métodos de prueba de igualdad de dos objetos: el método de instancia
pública Equals(Object) , el método público estático Equals(Object, Object) y el
método público estático ReferenceEquals(Object, Object) . De forma
predeterminada, estos métodos prueban la igualdad de referencia; es decir, para
que sean iguales, dos variables de objeto deben hacer referencia al mismo objeto.
El método público GetHashCode , que calcula un valor que permite que una
instancia del tipo se use en colecciones con hash.
El método público GetType , que devuelve un objeto Type que representa el tipo
SimpleClass .
El método protegido Finalize, que está diseñado para liberar recursos no
administrados antes de que el recolector de elementos no utilizados reclame la
memoria de un objeto.
El método protegido MemberwiseClone, que crea un clon superficial del objeto
actual.
Debido a la herencia implícita, se puede llamar a cualquier miembro heredado de un
objeto SimpleClass como si realmente fuera un miembro definido en la clase
SimpleClass . Así, en el ejemplo siguiente se llama al método SimpleClass.ToString , que
SimpleClass hereda de Object.
C#
public class EmptyClass
{ }
public class ClassNameExample
public static void Main()
EmptyClass sc = new();
Console.WriteLine(sc.ToString());
// The example displays the following output:
// EmptyClass
En la tabla siguiente se enumeran las categorías de tipos que se pueden crear en C# y
los tipos de los que heredan implícitamente. Cada tipo base constituye un conjunto
diferente de miembros disponible mediante herencia para los tipos derivados de forma
implícita.
Categoría de tipo Hereda implícitamente de
clase Object
struct ValueType, Object
enum Enum, ValueType, Object
delegado MulticastDelegate, Delegate, Object
Herencia y una relación "is a"
Normalmente, la herencia se usa para expresar una relación "is a" entre una clase base y
una o varias clases derivadas, donde las clases derivadas son versiones especializadas de
la clase base; la clase derivada es un tipo de la clase base. Por ejemplo, la clase
Publication representa una publicación de cualquier tipo y las clases Book y Magazine
representan tipos específicos de publicaciones.
7 Nota
Una clase o struct puede implementar una o varias interfaces. Aunque a menudo la
implementación se presenta como una solución alternativa para la herencia única o
como una forma de usar la herencia con structs, su finalidad es expresar una
relación diferente (una relación "can do") entre una interfaz y su tipo de
implementación que la herencia. Una interfaz define un subconjunto de
funcionalidad (por ejemplo, la posibilidad de probar la igualdad, comparar u
ordenar objetos o de admitir análisis y formato con referencia cultural) que la
interfaz pone a disposición de sus tipos de implementación.
Tenga en cuenta que "is a" también expresa la relación entre un tipo y una instancia
específica de ese tipo. En el ejemplo siguiente, Automobile es una clase que tiene tres
propiedades de solo lectura exclusivas: Make , el fabricante del automóvil; Model , el tipo
de automóvil; y Year , el año de fabricación. La clase Automobile también tiene un
constructor cuyos argumentos se asignan a los valores de propiedad, y reemplaza al
método Object.ToString para crear una cadena que identifica de forma única la instancia
de Automobile en lugar de la clase Automobile .
C#
public class Automobile
{
public Automobile(string make, string model, int year)
if (make == null)
throw new ArgumentNullException(nameof(make), "The make cannot
be null.");
else if (string.IsNullOrWhiteSpace(make))
throw new ArgumentException("make cannot be an empty string or
have space characters only.");
Make = make;
if (model == null)
throw new ArgumentNullException(nameof(model), "The model cannot
be null.");
else if (string.IsNullOrWhiteSpace(model))
throw new ArgumentException("model cannot be an empty string or
have space characters only.");
Model = model;
if (year < 1857 || year > DateTime.Now.Year + 2)
throw new ArgumentException("The year is out of range.");
Year = year;
public string Make { get; }
public string Model { get; }
public int Year { get; }
public override string ToString() => $"{Year} {Make} {Model}";
En este caso, no se debe basar en la herencia para representar marcas y modelos de
coche específicos. Por ejemplo, no es necesario definir un tipo Packard para representar
los automóviles fabricados por la empresa de automóviles Packard Motor. En su lugar,
se pueden representar mediante la creación de un objeto Automobile con los valores
adecuados que se pasan a su constructor de clase, como en el ejemplo siguiente.
C#
using System;
public class Example
public static void Main()
var packard = new Automobile("Packard", "Custom Eight", 1948);
Console.WriteLine(packard);
// The example displays the following output:
// 1948 Packard Custom Eight
Una relación "is a" basada en la herencia se aplica mejor a una clase base y a clases
derivadas que agregan miembros adicionales a la clase base o que requieren
funcionalidad adicional que no está presente en la clase base.
Diseño de la clase base y las clases derivadas
Veamos el proceso de diseño de una clase base y sus clases derivadas. En esta sección,
se definirá una clase base, Publication , que representa una publicación de cualquier
tipo, como un libro, una revista, un periódico, un diario, un artículo, etc. También se
definirá una clase Book que se deriva de Publication . El ejemplo se podría ampliar
fácilmente para definir otras clases derivadas, como Magazine , Journal , Newspaper y
Article .
Clase base Publication
A la hora de diseñar la clase Publication , se deben tomar varias decisiones de diseño:
Qué miembros se van a incluir en la clase Publication base, y si los miembros de
Publication proporcionan implementaciones de método, o bien si Publication es
una clase base abstracta que funciona como plantilla para sus clases derivadas.
En este caso, la clase Publication proporcionará implementaciones de método. La
sección Diseño de clases base abstractas y sus clases derivadas contiene un
ejemplo en el que se usa una clase base abstracta para definir los métodos que
deben invalidar las clases derivadas. Las clases derivadas pueden proporcionar
cualquier implementación que sea adecuada para el tipo derivado.
La posibilidad de reutilizar el código (es decir, varias clases derivadas comparten la
declaración y la implementación de los métodos de clase base y no tienen que
invalidarlos) es una ventaja de las clases base no abstractas. Por tanto, se deben
agregar miembros a Publication si es probable que algunos o la mayoría de los
tipos Publication especializados compartan su código. Si no puede proporcionar
implementaciones de clase base de forma eficaz, acabará por tener que
proporcionar implementaciones de miembros prácticamente idénticas en las clases
derivadas en lugar de una única implementación en la clase base. La necesidad de
mantener código duplicado en varias ubicaciones es un origen potencial de
errores.
Para maximizar la reutilización del código y crear una jerarquía de herencia lógica e
intuitiva, asegúrese de incluir en la clase Publication solo los datos y la
funcionalidad común a todas o a la mayoría de las publicaciones. Así, las clases
derivadas implementan miembros que son únicos para una clase determinada de
publicación que representan.
Hasta qué punto extender la jerarquía de clases. ¿Quiere desarrollar una jerarquía
de tres o más clases, en lugar de simplemente una clase base y una o más clases
derivadas? Por ejemplo, Publication podría ser una clase base de Periodical ,
que, a su vez, es una clase base de Magazine , Journal y Newspaper .
En el ejemplo, se usará la jerarquía pequeña de una clase Publication y una sola
clase derivada, Book . El ejemplo se podría ampliar fácilmente para crear una serie
de clases adicionales que se derivan de Publication , como Magazine y Article .
Si tiene sentido crear instancias de la clase base. Si no, se debe aplicar la palabra
clave abstract a la clase. De lo contrario, se puede crear una instancia de la clase
Publication mediante una llamada a su constructor de clase. Si se intenta crear
una instancia de una clase marcada con la palabra clave abstract mediante una
llamada directa a su constructor de clase, el compilador de C# genera el
error CS0144, "No se puede crear una instancia de la clase o interfaz abstracta". Si
se intenta crear una instancia de la clase mediante reflexión, el método de
reflexión produce una excepción MemberAccessException.
De forma predeterminada, se puede crear una instancia de una clase base
mediante una llamada a su constructor de clase. No es necesario definir un
constructor de clase de forma explícita. Si uno no está presente en el código
fuente de la clase base, el compilador de C# proporciona automáticamente un
constructor (sin parámetros) de forma predeterminada.
En el ejemplo, la clase Publication se marcará como abstract para que no se
puedan crear instancias de ella. Una clase abstract sin ningún método abstract
indica que representa un concepto abstracto que se comparte entre varias clases
concretas (como un Book , Journal ).
Si las clases derivadas deben heredar la implementación de la clase base de
determinados miembros, si tienen la opción de invalidar la implementación de la
clase base, o bien si deben proporcionar una implementación. La palabra clave
abstract se usa para forzar que las clases derivadas proporcionen una
implementación. La palabra clave virtual se usa para permitir que las clases
derivadas invaliden un método de clase base. De forma predeterminada, no se
pueden invalidar los métodos definidos en la clase base.
La clase Publication no tiene ningún método abstract , pero la propia clase es
abstract .
Si una clase derivada representa la clase final en la jerarquía de herencia y no se
puede usar ella misma como clase base para clases derivadas adicionales. De
forma predeterminada, cualquier clase puede servir como clase base. Se puede
aplicar la palabra clave sealed para indicar que una clase no puede servir como
clase base para las clases adicionales. Al intentar derivar de un error del
compilador generado por una clase sellada CS0509, "no se puede derivar de
typeName <>sellado".
Para el ejemplo, la clase derivada se marcará como sealed .
En el ejemplo siguiente se muestra el código fuente para la clase Publication , así como
una enumeración PublicationType que devuelve la propiedad
Publication.PublicationType . Además de los miembros que hereda de Object, la clase
Publication define los siguientes miembros únicos e invalidaciones de miembros:
C#
public enum PublicationType { Misc, Book, Magazine, Article };
public abstract class Publication
private bool _published = false;
private DateTime _datePublished;
private int _totalPages;
public Publication(string title, string publisher, PublicationType type)
if (string.IsNullOrWhiteSpace(publisher))
throw new ArgumentException("The publisher is required.");
Publisher = publisher;
if (string.IsNullOrWhiteSpace(title))
throw new ArgumentException("The title is required.");
Title = title;
Type = type;
public string Publisher { get; }
public string Title { get; }
public PublicationType Type { get; }
public string? CopyrightName { get; private set; }
public int CopyrightDate { get; private set; }
public int Pages
get { return _totalPages; }
set
if (value <= 0)
throw new ArgumentOutOfRangeException(nameof(value), "The
number of pages cannot be zero or negative.");
_totalPages = value;
public string GetPublicationDate()
if (!_published)
return "NYP";
else
return _datePublished.ToString("d");
public void Publish(DateTime datePublished)
_published = true;
_datePublished = datePublished;
public void Copyright(string copyrightName, int copyrightDate)
if (string.IsNullOrWhiteSpace(copyrightName))
throw new ArgumentException("The name of the copyright holder is
required.");
CopyrightName = copyrightName;
int currentYear = DateTime.Now.Year;
if (copyrightDate < currentYear - 10 || copyrightDate > currentYear
+ 2)
throw new ArgumentOutOfRangeException($"The copyright year must
be between {currentYear - 10} and {currentYear + 1}");
CopyrightDate = copyrightDate;
public override string ToString() => Title;
Un constructor
Dado que la clase Publication es abstract , no se puede crear una instancia de
ella directamente desde código similar al del ejemplo siguiente:
C#
var publication = new Publication("Tiddlywinks for Experts", "Fun and
Games",
PublicationType.Book);
Sin embargo, su constructor de instancia se puede llamar directamente desde los
constructores de clases derivadas, como muestra el código fuente de la clase Book .
Dos propiedades relacionadas con la publicación
Title es una propiedad String de solo lectura cuyo valor se suministra mediante la
llamada al constructor Publication .
Pages es una propiedad Int32 de solo lectura que indica cuántas páginas en total
tiene la publicación. El valor se almacena en un campo privado denominado
totalPages . Debe ser un número positivo o se inicia una excepción
ArgumentOutOfRangeException.
Miembros relacionados con el publicador
Dos propiedades de solo lectura, Publisher y Type . Los valores se proporcionan
originalmente mediante la llamada al constructor de clase Publication .
Miembros relacionados con la publicación
Dos métodos, Publish y GetPublicationDate , establecen y devuelven la fecha de
publicación. El método Publish establece una marca published privada en true
cuando se llama y asigna la fecha pasada a él como argumento al campo
datePublished privado. El método GetPublicationDate devuelve la cadena "NYP" si
la marca published es false , y el valor del campo datePublished si es true .
Miembros relacionados con copyright
El método Copyright toma como argumentos el nombre del propietario del
copyright y el año del copyright, y los asigna a las propiedades CopyrightName y
CopyrightDate .
Una invalidación del método ToString
Si un tipo no invalida al método Object.ToString, devuelve el nombre completo del
tipo, que es de poca utilidad a la hora de diferenciar una instancia de otra. La clase
Publication invalida Object.ToString para devolver el valor de la propiedad Title .
En la ilustración siguiente se muestra la relación entre la clase base Publication y su
clase Object heredada de forma implícita.
La clase Book .
La clase Book representa un libro como un tipo especializado de publicación. En el
ejemplo siguiente se muestra el código fuente de la clase Book .
C#
using System;
public sealed class Book : Publication
public Book(string title, string author, string publisher) :
this(title, string.Empty, author, publisher)
{ }
public Book(string title, string isbn, string author, string publisher)
: base(title, publisher, PublicationType.Book)
// isbn argument must be a 10- or 13-character numeric string
without "-" characters.
// We could also determine whether the ISBN is valid by comparing
its checksum digit
// with a computed checksum.
//
if (!string.IsNullOrEmpty(isbn))
// Determine if ISBN length is correct.
if (!(isbn.Length == 10 | isbn.Length == 13))
throw new ArgumentException("The ISBN must be a 10- or 13-
character numeric string.");
if (!ulong.TryParse(isbn, out _))
throw new ArgumentException("The ISBN can consist of numeric
characters only.");
ISBN = isbn;
Author = author;
public string ISBN { get; }
public string Author { get; }
public decimal Price { get; private set; }
// A three-digit ISO currency symbol.
public string? Currency { get; private set; }
// Returns the old price, and sets a new price.
public decimal SetPrice(decimal price, string currency)
if (price < 0)
throw new ArgumentOutOfRangeException(nameof(price), "The price
cannot be negative.");
decimal oldValue = Price;
Price = price;
if (currency.Length != 3)
throw new ArgumentException("The ISO currency symbol is a 3-
character string.");
Currency = currency;
return oldValue;
public override bool Equals(object? obj)
if (obj is not Book book)
return false;
else
return ISBN == book.ISBN;
public override int GetHashCode() => ISBN.GetHashCode();
public override string ToString() => $"{(string.IsNullOrEmpty(Author) ?
"" : Author + ", ")}{Title}";
Además de los miembros que hereda de Publication , la clase Book define los
siguientes miembros únicos e invalidaciones de miembros:
Dos constructores
Los dos constructores Book comparten tres parámetros comunes. Dos, title y
publisher, corresponden a los parámetros del constructor Publication . La tercera
es author, que se almacena para una propiedad Author pública inmutable. Un
constructor incluye un parámetro isbn, que se almacena en la propiedad
automática ISBN .
El primer constructor usa esta palabra clave para llamar al otro constructor. El
encadenamiento de constructores es un patrón común en la definición de
constructores. Los constructores con menos parámetros proporcionan valores
predeterminados al llamar al constructor con el mayor número de parámetros.
El segundo constructor usa la palabra clave base para pasar el título y el nombre
del editor al constructor de clase base. Si no realiza una llamada explícita a un
constructor de clase base en el código fuente, el compilador de C# proporciona
automáticamente una llamada al constructor sin parámetros o predeterminado de
la clase base.
Una propiedad ISBN de solo lectura, que devuelve el ISBN (International Standard
Book Number) del objeto Book , un número exclusivo de 10 y 13 caracteres. El ISBN
se proporciona como argumento para uno de los constructores Book . El ISBN se
almacena en un campo de respaldo privado, generado automáticamente por el
compilador.
Una propiedad Author de solo lectura. El nombre del autor se proporciona como
argumento para ambos constructores Book y se almacena en la propiedad.
Dos propiedades relacionadas con el precio de solo lectura, Price y Currency . Sus
valores se proporcionan como argumentos en una llamada al método SetPrice . La
propiedad Currency es el símbolo de moneda ISO de tres dígitos (por ejemplo,
USD para el dólar estadounidense). Los símbolos de moneda ISO se pueden
recuperar de la propiedad ISOCurrencySymbol. Ambas propiedades son de solo
lectura desde ubicaciones externas, pero se pueden establecer mediante código en
la clase Book .
Un método SetPrice , que establece los valores de las propiedades Price y
Currency . Esos son los valores devueltos por dichas propiedades.
Invalida el método ToString (heredado de Publication ) y los métodos
Object.Equals(Object) y GetHashCode (heredados de Object).
A menos que se invalide, el método Object.Equals(Object) prueba la igualdad de
referencia. Es decir, dos variables de objeto se consideran iguales si hacen
referencia al mismo objeto. Por otro lado, en la clase Book , dos objetos Book
deben ser iguales si tienen el mismo ISBN.
Cuando invalide el método Object.Equals(Object), también debe invalidar el
método GetHashCode, que devuelve un valor que se usa en el entorno de
ejecución para almacenar elementos en colecciones con hash para una
recuperación eficiente. El código hash debe devolver un valor que sea coherente
con la prueba de igualdad. Puesto que se ha invalidado Object.Equals(Object) para
devolver true , si las propiedades de ISBN de dos objetos Book son iguales, se
devuelve el código hash calculado mediante la llamada al método GetHashCode
de la cadena devuelta por la propiedad ISBN .
En la siguiente ilustración se muestra la relación entre la clase Book y Publication , su
clase base.
Ahora se puede crear una instancia de un objeto Book , invocar sus miembros únicos y
heredados, y pasarla como argumento a un método que espera un parámetro de tipo
Publication o de tipo Book , como se muestra en el ejemplo siguiente.
C#
public class ClassExample
public static void Main()
var book = new Book("The Tempest", "0971655819", "Shakespeare,
William",
"Public Domain Press");
ShowPublicationInfo(book);
book.Publish(new DateTime(2016, 8, 18));
ShowPublicationInfo(book);
var book2 = new Book("The Tempest", "Classic Works Press",
"Shakespeare, William");
Console.Write($"{book.Title} and {book2.Title} are the same
publication: " +
$"{((Publication)book).Equals(book2)}");
public static void ShowPublicationInfo(Publication pub)
string pubDate = pub.GetPublicationDate();
Console.WriteLine($"{pub.Title}, " +
$"{(pubDate == "NYP" ? "Not Yet Published" : "published on
" + pubDate):d} by {pub.Publisher}");
// The example displays the following output:
// The Tempest, Not Yet Published by Public Domain Press
// The Tempest, published on 8/18/2016 by Public Domain Press
// The Tempest and The Tempest are the same publication: False
Diseño de clases base abstractas y sus clases
derivadas
En el ejemplo anterior, se define una clase base que proporciona una implementación
para una serie de métodos con el fin de permitir que las clases derivadas compartan
código. En muchos casos, sin embargo, no se espera que la clase base proporcione una
implementación. En su lugar, la clase base es una clase abstracta que declara métodos
abstractos; sirve como una plantilla que define los miembros que debe implementar
cada clase derivada. Normalmente, en una clase base abstracta, la implementación de
cada tipo derivado es exclusiva de ese tipo. La clase se ha marcado con la palabra clave
abstract porque no tenía mucho sentido crear instancias de un objeto Publication ,
aunque la clase proporcionara las implementaciones de funcionalidad común a las
publicaciones.
Por ejemplo, cada forma geométrica bidimensional cerrada incluye dos propiedades:
área, la extensión interna de la forma; y perímetro, o la distancia a lo largo de los bordes
de la forma. La manera en que se calculan estas propiedades, sin embargo, depende
completamente de la forma específica. La fórmula para calcular el perímetro (o la
circunferencia) de un círculo, por ejemplo, es diferente a la de un cuadrado. La clase
Shape es una clase abstract con métodos abstract . Eso indica que las clases derivadas
comparten la misma funcionalidad, pero que la implementan de otra manera.
En el ejemplo siguiente se define una clase base abstracta denominada Shape que
define dos propiedades: Area y Perimeter . Además de marcar la clase con la palabra
clave abstract, cada miembro de instancia también se marca con la palabra clave
abstract. En este caso, Shape también invalida el método Object.ToString para devolver
el nombre del tipo, en lugar de su nombre completo. Y define dos miembros estáticos,
GetArea y GetPerimeter , que permiten que los llamadores recuperen fácilmente el área
y el perímetro de una instancia de cualquier clase derivada. Cuando se pasa una
instancia de una clase derivada a cualquiera de estos métodos, el runtime llama a la
invalidación del método de la clase derivada.
C#
public abstract class Shape
public abstract double Area { get; }
public abstract double Perimeter { get; }
public override string ToString() => GetType().Name;
public static double GetArea(Shape shape) => shape.Area;
public static double GetPerimeter(Shape shape) => shape.Perimeter;
Después, se pueden derivar algunas clases de Shape que representan formas concretas.
El ejemplo siguiente define tres clases Square , Rectangle y Circle . Cada una usa una
fórmula única para esa forma en particular para calcular el área y el perímetro. Algunas
de las clases derivadas también definen propiedades, como Rectangle.Diagonal y
Circle.Diameter , que son únicas para la forma que representan.
C#
using System;
public class Square : Shape
public Square(double length)
Side = length;
public double Side { get; }
public override double Area => Math.Pow(Side, 2);
public override double Perimeter => Side * 4;
public double Diagonal => Math.Round(Math.Sqrt(2) * Side, 2);
public class Rectangle : Shape
public Rectangle(double length, double width)
Length = length;
Width = width;
public double Length { get; }
public double Width { get; }
public override double Area => Length * Width;
public override double Perimeter => 2 * Length + 2 * Width;
public bool IsSquare() => Length == Width;
public double Diagonal => Math.Round(Math.Sqrt(Math.Pow(Length, 2) +
Math.Pow(Width, 2)), 2);
public class Circle : Shape
public Circle(double radius)
Radius = radius;
public override double Area => Math.Round(Math.PI * Math.Pow(Radius, 2),
2);
public override double Perimeter => Math.Round(Math.PI * 2 * Radius, 2);
// Define a circumference, since it's the more familiar term.
public double Circumference => Perimeter;
public double Radius { get; }
public double Diameter => Radius * 2;
En el ejemplo siguiente se usan objetos derivados de Shape . Se crea una instancia de
una matriz de objetos derivados de Shape y se llama a los métodos estáticos de la clase
Shape , que ajusta los valores de propiedad Shape devueltos. El runtime recupera los
valores de las propiedades invalidadas de los tipos derivados. En el ejemplo también se
convierte cada objeto Shape de la matriz a su tipo derivado y, si la conversión se realiza
correctamente, recupera las propiedades de esa subclase específica de Shape .
C#
using System;
public class Example
public static void Main()
Shape[] shapes = { new Rectangle(10, 12), new Square(5),
new Circle(3) };
foreach (Shape shape in shapes)
Console.WriteLine($"{shape}: area, {Shape.GetArea(shape)}; " +
$"perimeter, {Shape.GetPerimeter(shape)}");
if (shape is Rectangle rect)
Console.WriteLine($" Is Square: {rect.IsSquare()},
Diagonal: {rect.Diagonal}");
continue;
if (shape is Square sq)
Console.WriteLine($" Diagonal: {sq.Diagonal}");
continue;
// The example displays the following output:
// Rectangle: area, 120; perimeter, 44
// Is Square: False, Diagonal: 15.62
// Square: area, 25; perimeter, 20
// Diagonal: 7.07
// Circle: area, 28.27; perimeter, 18.85
Procedimiento para convertir de forma
segura mediante la coincidencia de
patrones y los operadores is y as
Artículo • 15/02/2023 • Tiempo de lectura: 4 minutos
Dado que los objetos son polimórficos, es posible que una variable de un tipo de clase
base contenga un tipo derivado. Para acceder a los miembros de instancia del tipo
derivado, es necesario volver a convertir el valor en el tipo derivado. Pero una
conversión conlleva el riesgo de producir una InvalidCastException. C# proporciona
instrucciones de coincidencia de patrones que realizan una conversión
condicionalmente, solo si se va a realizar correctamente. C# además proporciona los
operadores is y as para probar si un valor es de un tipo determinado.
En el ejemplo siguiente se muestra cómo usar la instrucción is de coincidencia de
patrones.
C#
var g = new Giraffe();
var a = new Animal();
FeedMammals(g);
FeedMammals(a);
// Output:
// Eating.
// Animal is not a Mammal
SuperNova sn = new SuperNova();
TestForMammals(g);
TestForMammals(sn);
static void FeedMammals(Animal a)
if (a is Mammal m)
m.Eat();
else
// variable 'm' is not in scope here, and can't be used.
Console.WriteLine($"{a.GetType().Name} is not a Mammal");
static void TestForMammals(object o)
// You also can use the as operator and test for null
// before referencing the variable.
var m = o as Mammal;
if (m != null)
Console.WriteLine(m.ToString());
else
Console.WriteLine($"{o.GetType().Name} is not a Mammal");
// Output:
// I am an animal.
// SuperNova is not a Mammal
class Animal
public void Eat() { Console.WriteLine("Eating."); }
public override string ToString()
return "I am an animal.";
class Mammal : Animal { }
class Giraffe : Mammal { }
class SuperNova { }
El ejemplo anterior muestra una serie de características de sintaxis de coincidencia de
patrones. La instrucción if (a is Mammal m) combina la prueba con una asignación de
inicialización. La asignación solo se produce cuando la prueba se realiza correctamente.
La variable m solo está en ámbito en la instrucción if insertada donde se ha asignado.
No se puede acceder a m más adelante en el mismo método. En el ejemplo anterior
también se muestra cómo usar el operador as para convertir un objeto en un tipo
especificado.
También puede usar la misma sintaxis para probar si un tipo de valor que admite valores
NULL tiene un valor, como se muestra en el ejemplo siguiente:
C#
int i = 5;
PatternMatchingNullable(i);
int? j = null;
PatternMatchingNullable(j);
double d = 9.78654;
PatternMatchingNullable(d);
PatternMatchingSwitch(i);
PatternMatchingSwitch(j);
PatternMatchingSwitch(d);
static void PatternMatchingNullable(ValueType? val)
if (val is int j) // Nullable types are not allowed in patterns
Console.WriteLine(j);
else if (val is null) // If val is a nullable type with no value, this
expression is true
Console.WriteLine("val is a nullable type with the null value");
else
Console.WriteLine("Could not convert " + val.ToString());
static void PatternMatchingSwitch(ValueType? val)
switch (val)
case int number:
Console.WriteLine(number);
break;
case long number:
Console.WriteLine(number);
break;
case decimal number:
Console.WriteLine(number);
break;
case float number:
Console.WriteLine(number);
break;
case double number:
Console.WriteLine(number);
break;
case null:
Console.WriteLine("val is a nullable type with the null value");
break;
default:
Console.WriteLine("Could not convert " + val.ToString());
break;
El ejemplo anterior muestra otras características de coincidencia de patrones para usar
con conversiones. Puede probar una variable para el patrón null si busca
específicamente el valor null . Cuando el valor de runtime de la variable es null , una
instrucción is que busca un tipo siempre devuelve false . La instrucción is de
coincidencia de patrones no permite un tipo de valor que acepta valores NULL, como
int? o Nullable<int> , pero puede probar con cualquier otro tipo de valor. Los patrones
is del ejemplo anterior no se limitan a los tipos de valor que admiten un valor NULL.
También puede usar esos patrones para probar si una variable de un tipo de referencia
tiene un valor o es null .
En el ejemplo anterior también se muestra cómo usar el patrón de tipo en una
instrucción switch donde la variable puede ser uno de muchos tipos diferentes.
Si quiere probar si una variable es de un tipo determinado, pero no asignarla a una
nueva variable, puede usar los operadores is y as para los tipos de referencia y los
tipos que aceptan valores NULL. El código siguiente muestra cómo usar las instrucciones
is y as que formaban parte del lenguaje C# antes de la incorporación de la
coincidencia de patrones para probar si una variable es de un tipo determinado:
C#
// Use the is operator to verify the type.
// before performing a cast.
Giraffe g = new();
UseIsOperator(g);
// Use the as operator and test for null
// before referencing the variable.
UseAsOperator(g);
// Use pattern matching to test for null
// before referencing the variable
UsePatternMatchingIs(g);
// Use the as operator to test
// an incompatible type.
SuperNova sn = new();
UseAsOperator(sn);
// Use the as operator with a value type.
// Note the implicit conversion to int? in
// the method body.
int i = 5;
UseAsWithNullable(i);
double d = 9.78654;
UseAsWithNullable(d);
static void UseIsOperator(Animal a)
if (a is Mammal)
Mammal m = (Mammal)a;
m.Eat();
static void UsePatternMatchingIs(Animal a)
if (a is Mammal m)
m.Eat();
static void UseAsOperator(object o)
Mammal? m = o as Mammal;
if (m is not null)
Console.WriteLine(m.ToString());
else
Console.WriteLine($"{o.GetType().Name} is not a Mammal");
static void UseAsWithNullable(System.ValueType val)
int? j = val as int?;
if (j is not null)
Console.WriteLine(j);
else
Console.WriteLine("Could not convert " + val.ToString());
class Animal
public void Eat() => Console.WriteLine("Eating.");
public override string ToString() => "I am an animal.";
class Mammal : Animal { }
class Giraffe : Mammal { }
class SuperNova { }
Como puede ver al comparar este código con el código de coincidencia de patrones, la
sintaxis de coincidencia de patrones proporciona características más sólidas mediante la
combinación de la prueba y la asignación en una sola instrucción. Use la sintaxis de
coincidencia de patrones siempre que sea posible.
Tutorial: Uso de la coincidencia de
patrones para compilar algoritmos
basados en tipos y basados en datos
Artículo • 20/02/2023 • Tiempo de lectura: 17 minutos
Puede escribir una funcionalidad que se comporta como si se hubiesen ampliado tipos
que pueden estar en otras bibliotecas. Los patrones también se usan para crear una
funcionalidad que la aplicación requiere y que no es una característica fundamental del
tipo que se está ampliando.
En este tutorial, aprenderá a:
" Reconocer situaciones donde se debe usar la coincidencia de patrones.
" Usar las expresiones de coincidencia de patrones para implementar un
comportamiento en función de los tipos y los valores de propiedad.
" Combinar la coincidencia de patrones con otras técnicas para crear algoritmos
completos.
Requisitos previos
Se recomienda tener Visual Studio para Windows o Mac. Puede descargar una
versión gratuita desde la página de descargas de Visual Studio . Visual Studio
incluye el SDK de .NET.
También se puede usar el editor de Visual Studio Code . Deberá instalar el SDK
de .NET más reciente por separado.
Si prefiere usar otro editor, deberá instalar el SDK de .NET más reciente.
En este tutorial se da por supuesto que conoce bien C# y. NET, incluidos Visual Studio o
la CLI de .NET.
Escenarios para la coincidencia de patrones
Con frecuencia, el desarrollo moderno incluye integrar datos desde varios orígenes y
presentar información y perspectivas a partir de esos datos en una sola aplicación
cohesiva. Ni usted ni su equipo tendrán control ni acceso para todos los tipos que
representan los datos entrantes.
El diseño clásico orientado a objetos llamaría a la creación de tipos en la aplicación que
representen cada tipo de datos de esos orígenes de datos múltiples. Luego, la aplicación
podría trabajar con esos tipos nuevos, crear jerarquías de herencia, crear métodos
virtuales e implementar abstracciones. Esas técnicas funcionan y son a veces las mejores
herramientas. En otras ocasiones, puede escribir menos código. Puede escribir código
más claro con técnicas que separan los datos de las operaciones que manipulan esos
datos.
En este tutorial, creará y explorará una aplicación que toma datos entrantes de varios
orígenes externos para un solo escenario. Verá cómo la coincidencia de patrones
proporciona una forma eficaz de consumir y procesar esos datos de maneras que no
formaban parte del sistema original.
Considere un área metropolitana importante que usa peajes y precios estipulados para
las horas de mayor actividad con el fin de administrar el tráfico. Puede escribir una
aplicación que calcule los peajes de un vehículo en función de su tipo. Mejoras
posteriores incorporan precios basados en la cantidad de ocupantes del vehículo. Otras
mejoras agregan precios según la hora y el día de la semana.
Desde esa descripción breve, puede haber esbozado rápidamente una jerarquía de
objetos para modelar este sistema. Sin embargo, los datos provienen de varios orígenes,
como otros sistemas de administración de registros de vehículos. Estos sistemas ofrecen
distintas clases para modelar esos datos y no se tiene un modelo de objetos único que
puede usar. En este tutorial, usará estas clases simplificadas para modelar los datos del
vehículo desde dichos sistemas externos, tal como se muestra en el código siguiente:
C#
namespace ConsumerVehicleRegistration
public class Car
public int Passengers { get; set; }
namespace CommercialRegistration
public class DeliveryTruck
public int GrossWeightClass { get; set; }
namespace LiveryRegistration
public class Taxi
public int Fares { get; set; }
public class Bus
public int Capacity { get; set; }
public int Riders { get; set; }
Puede descargar el código de inicio del repositorio dotnet/samples de GitHub. Puede
ver que las clases de vehículo provienen de distintos sistemas y que están en distintos
espacios de nombres. No se puede usar ninguna clase base común distinta de
System.Object .
Diseños de coincidencia de patrones
En el escenario que se usa en este tutorial se resaltan los tipos de problemas en los que
resulta adecuado usar la coincidencia de patrones para resolver lo siguiente:
Los objetos con los que necesita trabajar no están en una jerarquía de objetos que
coincida con sus objetivos. Es posible que trabaje con clases que forman parte de
sistemas no relacionados.
La funcionalidad que agrega no forma parte de la abstracción central de estas
clases. El peaje que paga un vehículo cambia según los distintos tipos de vehículos,
pero el peaje no es una función central del vehículo.
Cuando la forma de los datos y las operaciones que se realizan en esos datos no se
describen en conjunto, las características de coincidencia de patrones de C# permiten
que sea más fácil trabajar con ellos.
Implementación de cálculos de peajes básicos
El cálculo de peaje más básico solo se basa en el tipo de vehículo:
Un Car es USD 2,00.
Un Taxi es USD 3,50.
Un Bus es USD 5,00.
Un DeliveryTruck es USD 10,00
Cree una clase TollCalculator nueva e implemente la coincidencia de patrones en el
tipo de vehículo para obtener el importe del peaje. En el siguiente código se muestra la
implementación inicial de TollCalculator .
C#
using System;
using CommercialRegistration;
using ConsumerVehicleRegistration;
using LiveryRegistration;
namespace Calculators;
public class TollCalculator
public decimal CalculateToll(object vehicle) =>
vehicle switch
Car c => 2.00m,
Taxi t => 3.50m,
Bus b => 5.00m,
DeliveryTruck t => 10.00m,
{ } => throw new ArgumentException(message: "Not a known
vehicle type", paramName: nameof(vehicle)),
null => throw new ArgumentNullException(nameof(vehicle))
};
El código anterior usa una expresión switch (que no es lo mismo que una instrucción
switch) que prueba el patrón de declaración. Una expresión switch comienza por la
variable, vehicle en el código anterior, seguida de la palabra clave switch . A
continuación, todos los segmentos modificadores aparecen entre llaves. La expresión
switch lleva a cabo otras mejoras en la sintaxis que rodea la instrucción switch . La
palabra clave case se omite y el resultado de cada segmento es una expresión. Los dos
últimos segmentos muestran una característica de lenguaje nueva. El caso { } coincide
con cualquier objeto no nulo que no coincidía con ningún segmento anterior. Este
segmento detecta todo tipo incorrecto que se pasa a este método. El caso { } debe
seguir los casos de cada tipo de vehículo. Si se invierte el orden, el caso { } tendrá
prioridad. Por último, el null patrón de constante detecta si se pasa null a este
método. El patrón null puede ser el último porque los otros patrones solo coinciden
con un objeto no nulo del tipo correcto.
Puede probar este código con el código siguiente en Program.cs :
C#
using System;
using CommercialRegistration;
using ConsumerVehicleRegistration;
using LiveryRegistration;
using toll_calculator;
var tollCalc = new TollCalculator();
var car = new Car();
var taxi = new Taxi();
var bus = new Bus();
var truck = new DeliveryTruck();
Console.WriteLine($"The toll for a car is {tollCalc.CalculateToll(car)}");
Console.WriteLine($"The toll for a taxi is {tollCalc.CalculateToll(taxi)}");
Console.WriteLine($"The toll for a bus is {tollCalc.CalculateToll(bus)}");
Console.WriteLine($"The toll for a truck is
{tollCalc.CalculateToll(truck)}");
try
tollCalc.CalculateToll("this will fail");
catch (ArgumentException e)
Console.WriteLine("Caught an argument exception when using the wrong
type");
try
tollCalc.CalculateToll(null!);
catch (ArgumentNullException e)
Console.WriteLine("Caught an argument exception when using null");
Ese código se incluye en el proyecto de inicio, pero se marca como comentario. Quite
los comentarios y podrá probar lo que escribió.
Empezará a ver cómo los patrones pueden ayudarlo a crear algoritmos cuando el
código y los datos están separados. La expresión switch prueba el tipo y genera valores
distintos en función de los resultados. Eso es solo el principio.
Incorporación de precios por ocupación
La autoridad encargada de los peajes quiere incentivar que los vehículos viajen a plena
capacidad. Se decidió cobrar más cuando los vehículos circulan con menos pasajeros y
se incentiva que los vehículos vayan llenos al ofrecer precios más bajos:
Los automóviles y taxis sin pasajeros pagan USD 0,50 adicionales.
Los automóviles y taxis con dos pasajeros tienen un descuento de USD 0,50.
Los automóviles y taxis con tres o más pasajeros tienen un descuento de USD 1.
Los buses que viajan con menos del 50 % de su capacidad pagan USD 2
adicionales.
Los buses que viajan con más del 90 % de su capacidad tienen un descuento de
USD 1.
Estas reglas se pueden implementar con un patrón de propiedad en la misma expresión
switch. Un patrón de propiedad compara un valor de propiedad con un valor constante.
El patrón de propiedad examina las propiedades del objeto una vez que se determina el
tipo. El caso único de Car se amplía a cuatro casos distintos:
C#
vehicle switch
Car {Passengers: 0} => 2.00m + 0.50m,
Car {Passengers: 1} => 2.0m,
Car {Passengers: 2} => 2.0m - 0.50m,
Car => 2.00m - 1.0m,
// ...
};
Los primeros tres casos prueban el tipo como Car y luego comprueban el valor de la
propiedad Passengers . Si ambos coinciden, esa expresión se evalúa y devuelve.
También podría expandir los casos para los taxis de manera similar:
C#
vehicle switch
// ...
Taxi {Fares: 0} => 3.50m + 1.00m,
Taxi {Fares: 1} => 3.50m,
Taxi {Fares: 2} => 3.50m - 0.50m,
Taxi => 3.50m - 1.00m,
// ...
};
A continuación, implemente las reglas de ocupación mediante la expansión de los casos
para los buses, tal como se muestra en el ejemplo siguiente:
C#
vehicle switch
// ...
Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m +
2.00m,
Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m -
1.00m,
Bus => 5.00m,
// ...
};
A la autoridad encargada de los peajes no le preocupa el número de pasajeros en los
camiones de reparto. Alternativamente, ajustan el importe del peaje en base a la clase
de peso de los camiones, como sigue:
A los camiones de más de 2268 kilos se les cobran USD 5 adicionales.
Los camiones livianos, por debajo de los 1360 kilos, tienen un descuento de 2 USD.
Esta regla se implementa con el código siguiente:
C#
vehicle switch
// ...
DeliveryTruck t when (t.GrossWeightClass > 5000) => 10.00m + 5.00m,
DeliveryTruck t when (t.GrossWeightClass < 3000) => 10.00m - 2.00m,
DeliveryTruck => 10.00m,
};
En el código anterior se muestra la cláusula when de un segmento modificador. Puede
usar la cláusula when para probar condiciones distintas de la igualdad de una propiedad.
Cuando haya terminado, tendrá un método muy similar al código siguiente:
C#
vehicle switch
Car {Passengers: 0} => 2.00m + 0.50m,
Car {Passengers: 1} => 2.0m,
Car {Passengers: 2} => 2.0m - 0.50m,
Car => 2.00m - 1.0m,
Taxi {Fares: 0} => 3.50m + 1.00m,
Taxi {Fares: 1} => 3.50m,
Taxi {Fares: 2} => 3.50m - 0.50m,
Taxi => 3.50m - 1.00m,
Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m +
2.00m,
Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m -
1.00m,
Bus => 5.00m,
DeliveryTruck t when (t.GrossWeightClass > 5000) => 10.00m + 5.00m,
DeliveryTruck t when (t.GrossWeightClass < 3000) => 10.00m - 2.00m,
DeliveryTruck => 10.00m,
{ } => throw new ArgumentException(message: "Not a known vehicle
type", paramName: nameof(vehicle)),
null => throw new ArgumentNullException(nameof(vehicle))
};
Muchos de estos segmentos modificadores son ejemplos de patrones recursivos. Por
ejemplo, Car { Passengers: 1} muestra un patrón constante dentro de un patrón de
propiedad.
Puede usar modificadores anidados para que este código sea menos repetitivo. Tanto
Car como Taxi tienen cuatro segmentos distintos en los ejemplos anteriores. En ambos
casos, se puede crear un patrón de declaración que se alimenta de un patrón de
constante. Esta técnica se muestra en el código siguiente:
C#
public decimal CalculateToll(object vehicle) =>
vehicle switch
Car c => c.Passengers switch
0 => 2.00m + 0.5m,
1 => 2.0m,
2 => 2.0m - 0.5m,
_ => 2.00m - 1.0m
},
Taxi t => t.Fares switch
0 => 3.50m + 1.00m,
1 => 3.50m,
2 => 3.50m - 0.50m,
_ => 3.50m - 1.00m
},
Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m +
2.00m,
Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m -
1.00m,
Bus b => 5.00m,
DeliveryTruck t when (t.GrossWeightClass > 5000) => 10.00m + 5.00m,
DeliveryTruck t when (t.GrossWeightClass < 3000) => 10.00m - 2.00m,
DeliveryTruck t => 10.00m,
{ } => throw new ArgumentException(message: "Not a known vehicle
type", paramName: nameof(vehicle)),
null => throw new ArgumentNullException(nameof(vehicle))
};
En el ejemplo anterior, el uso de una expresión recursiva significa que no repite los
segmentos Car y Taxi que contienen segmentos secundarios que prueban el valor de
propiedad. Esta técnica no se usa para los segmentos Bus y DeliveryTruck , porque esos
segmentos prueban intervalos para la propiedad, no valores discretos.
Incorporación de precio en horas punta
Para la característica final, la autoridad encargada de los peajes quiere agregar precios
en función de las horas punta. En las horas de mayor afluencia durante mañana y tarde,
el valor de los peajes se dobla. Esa regla solo afecta el tráfico en una dirección: hacia la
ciudad en la mañana y desde la ciudad en la tarde. En otros momentos durante la
jornada laboral, los peajes aumentan en un 50 %. Tarde por la noche y temprano en la
mañana, disminuyen en un 25 %. Durante el fin de semana, la tarifa es normal
independientemente de la hora. Puede usar una serie de instrucciones if y else para
expresar esto mediante el código siguiente:
C#
public decimal PeakTimePremiumIfElse(DateTime timeOfToll, bool inbound)
if ((timeOfToll.DayOfWeek == DayOfWeek.Saturday) ||
(timeOfToll.DayOfWeek == DayOfWeek.Sunday))
return 1.0m;
else
int hour = timeOfToll.Hour;
if (hour < 6)
return 0.75m;
else if (hour < 10)
if (inbound)
return 2.0m;
else
return 1.0m;
else if (hour < 16)
return 1.5m;
else if (hour < 20)
if (inbound)
return 1.0m;
else
return 2.0m;
else // Overnight
return 0.75m;
El código anterior funciona correctamente, pero no es legible. Para que el código tenga
sentido, tiene que encadenar todos los casos de entrada y las instrucciones if
anidadas. En su lugar, usará la coincidencia de patrones para esta característica, pero la
integrará con otras técnicas. Podría crear una expresión de coincidencia de patrones
única que consideraría todas las combinaciones de dirección, día de la semana y hora. El
resultado sería una expresión complicada. Podría ser difícil de leer y de comprender.
Esto implica que es difícil garantizar su exactitud. En su lugar, combine esos método
para crear una tupla de valores que describa de manera concisa todos esos estados.
Luego, use la coincidencia de patrones para calcular un multiplicador para el peaje. La
tupla contiene tres condiciones discretas:
El día es un día laborable o fin de semana.
La banda de tiempo cuando se cobra el peaje.
La dirección si va hacia la ciudad o desde la ciudad.
En la tabla siguiente se muestran las combinaciones de valores de entrada y el
multiplicador de precio en horas punta:
Día Time Dirección Premium
Día Time Dirección Premium
Día de la semana hora punta de la mañana hacia la ciudad x 2,00
Día de la semana hora punta de la mañana desde la ciudad x 1,00
Día de la semana día hacia la ciudad x 1,50
Día de la semana día desde la ciudad x 1,50
Día de la semana hora punta de la tarde hacia la ciudad x 1,00
Día de la semana hora punta de la tarde desde la ciudad x 2,00
Día de la semana noche hacia la ciudad x 0,75
Día de la semana noche desde la ciudad x 0,75
Fin de semana hora punta de la mañana hacia la ciudad x 1,00
Fin de semana hora punta de la mañana desde la ciudad x 1,00
Fin de semana día hacia la ciudad x 1,00
Fin de semana día desde la ciudad x 1,00
Fin de semana hora punta de la tarde hacia la ciudad x 1,00
Fin de semana hora punta de la tarde desde la ciudad x 1,00
Fin de semana noche hacia la ciudad x 1,00
Fin de semana noche desde la ciudad x 1,00
Hay 16 combinaciones distintas de las tres variables. Mediante la combinación de
algunas de las condiciones, simplificará la expresión switch final.
El sistema que cobra los peajes usa una estructura DateTime para la hora en que se
cobró el peaje. Genere métodos de miembro que creen las variables a partir de la tabla
anterior. La función siguiente usa una expresión switch de coincidencia de patrones para
expresar si la estructura DateTime representa un día laborable o un fin de semana:
C#
private static bool IsWeekDay(DateTime timeOfToll) =>
timeOfToll.DayOfWeek switch
DayOfWeek.Monday => true,
DayOfWeek.Tuesday => true,
DayOfWeek.Wednesday => true,
DayOfWeek.Thursday => true,
DayOfWeek.Friday => true,
DayOfWeek.Saturday => false,
DayOfWeek.Sunday => false
};
Ese método es correcto, pero es redundante. Puede simplificarlo tal como se muestra en
el código siguiente:
C#
private static bool IsWeekDay(DateTime timeOfToll) =>
timeOfToll.DayOfWeek switch
DayOfWeek.Saturday => false,
DayOfWeek.Sunday => false,
_ => true
};
A continuación, agregue una función similar para categorizar la hora en los bloques:
C#
private enum TimeBand
MorningRush,
Daytime,
EveningRush,
Overnight
private static TimeBand GetTimeBand(DateTime timeOfToll) =>
timeOfToll.Hour switch
< 6 or > 19 => TimeBand.Overnight,
< 10 => TimeBand.MorningRush,
< 16 => TimeBand.Daytime,
_ => TimeBand.EveningRush,
};
Agregue un elemento enum privado para convertir cada intervalo de tiempo en un valor
discreto. Después, el método GetTimeBand usa patrones relacionales y patrones or
conjuntivos, ambos agregados en C# 9.0. El patrón relacional permite probar un valor
numérico mediante < , > , <= o >= . El patrón or comprueba si una expresión coincide
con uno o más patrones. También puede usar un patrón and para asegurarse de que
una expresión coincide con dos patrones distintos, y un patrón not para probar que una
expresión no coincide con un patrón.
Después de usar esos métodos, puede usar otra expresión switch con el patrón de
tuplas para calcular el recargo en los precios. Puede crear una expresión switch con los
16 segmentos:
C#
public decimal PeakTimePremiumFull(DateTime timeOfToll, bool inbound) =>
(IsWeekDay(timeOfToll), GetTimeBand(timeOfToll), inbound) switch
(true, TimeBand.MorningRush, true) => 2.00m,
(true, TimeBand.MorningRush, false) => 1.00m,
(true, TimeBand.Daytime, true) => 1.50m,
(true, TimeBand.Daytime, false) => 1.50m,
(true, TimeBand.EveningRush, true) => 1.00m,
(true, TimeBand.EveningRush, false) => 2.00m,
(true, TimeBand.Overnight, true) => 0.75m,
(true, TimeBand.Overnight, false) => 0.75m,
(false, TimeBand.MorningRush, true) => 1.00m,
(false, TimeBand.MorningRush, false) => 1.00m,
(false, TimeBand.Daytime, true) => 1.00m,
(false, TimeBand.Daytime, false) => 1.00m,
(false, TimeBand.EveningRush, true) => 1.00m,
(false, TimeBand.EveningRush, false) => 1.00m,
(false, TimeBand.Overnight, true) => 1.00m,
(false, TimeBand.Overnight, false) => 1.00m,
};
El código anterior funciona, pero se puede simplificar. Las ocho combinaciones para el
fin de semana tienen el mismo peaje. Puede reemplazar las ocho por la siguiente línea:
C#
(false, _, _) => 1.0m,
Tanto el tráfico hacia la ciudad como el tráfico desde la ciudad tienen el mismo
multiplicador durante el día y la noche de los fines de semana. Esos cuatro segmentos
modificadores se pueden reemplazar por las dos líneas siguientes:
C#
(true, TimeBand.Overnight, _) => 0.75m,
(true, TimeBand.Daytime, _) => 1.5m,
El código debe ser similar al código siguiente después de esos dos cambios:
C#
public decimal PeakTimePremium(DateTime timeOfToll, bool inbound) =>
(IsWeekDay(timeOfToll), GetTimeBand(timeOfToll), inbound) switch
(true, TimeBand.MorningRush, true) => 2.00m,
(true, TimeBand.MorningRush, false) => 1.00m,
(true, TimeBand.Daytime, _) => 1.50m,
(true, TimeBand.EveningRush, true) => 1.00m,
(true, TimeBand.EveningRush, false) => 2.00m,
(true, TimeBand.Overnight, _) => 0.75m,
(false, _, _) => 1.00m,
};
Por último, puede quitar las dos horas punta en que se paga el precio regular. Cuando
quite esos segmentos, puede reemplazar false por un descarte ( _ ) en el segmento
modificador final. Tendrá el siguiente método finalizado:
C#
public decimal PeakTimePremium(DateTime timeOfToll, bool inbound) =>
(IsWeekDay(timeOfToll), GetTimeBand(timeOfToll), inbound) switch
(true, TimeBand.Overnight, _) => 0.75m,
(true, TimeBand.Daytime, _) => 1.5m,
(true, TimeBand.MorningRush, true) => 2.0m,
(true, TimeBand.EveningRush, false) => 2.0m,
_ => 1.0m,
};
En este ejemplo se resalta una de las ventajas de la coincidencia de patrones: las ramas
del patrón se evalúan en orden. Si vuelve a ordenarlas para que una rama anterior
controle uno de los últimos casos, el compilador genera una advertencia sobre el
código inaccesible. Esas reglas de lenguaje facilitan la realización de las simplificaciones
anteriores con la confianza de que el código no cambió.
La coincidencia de patrones hace que algunos tipos de código sean más legibles y
ofrece una alternativa a las técnicas orientadas a objetos cuando no se puede agregar
código a las clases. La nube hace que los datos y la funcionalidad residan por separado.
La forma de los datos y las operaciones que se realizan en ellos no necesariamente se
describen en conjunto. En este tutorial, consumió datos existentes de maneras
totalmente distintas de su función original. La coincidencia de patrones le ha brindado
la capacidad de escribir una funcionalidad que reemplazase a esos tipos, aunque no le
permitía extenderlos.
Pasos siguientes
Puede descargar el código finalizado del repositorio GitHub dotnet/samples . Explore
los patrones por su cuenta y agregue esta técnica a sus actividades habituales de
codificación. Aprender estas técnicas le permite contar con otra forma de enfocar los
problemas y crear una funcionalidad nueva.
Vea también
Patrones
Expresión switch
Procedimientos para controlar una
excepción mediante Try y Catch
Artículo • 15/02/2023 • Tiempo de lectura: 2 minutos
El propósito de un bloque try-catch es detectar y controlar una excepción generada por
código en funcionamiento. Algunas excepciones se pueden controlar en un bloque
catch y es posible resolver el problema sin que se vuelva a producir la excepción, pero
la mayoría de las veces lo único que se puede hacer es asegurarse de que se produzca
la excepción adecuada.
Ejemplo
En este ejemplo, IndexOutOfRangeException no es la excepción más adecuada. Tiene
más sentido la excepción ArgumentOutOfRangeException para el método, ya que el
error lo provoca el argumento index que pasa el autor de la llamada.
C#
static int GetInt(int[] array, int index)
try
return array[index];
catch (IndexOutOfRangeException e) // CS0168
Console.WriteLine(e.Message);
// Set IndexOutOfRangeException to the new exception's
InnerException.
throw new ArgumentOutOfRangeException("index parameter is out of
range.", e);
Comentarios
El código que produce una excepción está incluido en el bloque try . Se agrega una
instrucción catch inmediatamente después para controlar IndexOutOfRangeException , si
se produce. El bloque catch controla la excepción IndexOutOfRangeException y produce
en su lugar la excepción ArgumentOutOfRangeException , más adecuada. Para
proporcionar al autor de la llamada tanta información como sea posible, considere la
posibilidad de especificar la excepción original como InnerException de la nueva
excepción. Dado que la propiedad InnerException es read-only, debe asignarla en el
constructor de la nueva excepción.
Procedimiento para ejecutar código de
limpieza mediante finally
Artículo • 15/02/2023 • Tiempo de lectura: 2 minutos
El propósito de una instrucción finally es asegurarse de que la limpieza necesaria de
los objetos, por lo general objetos que contienen recursos externos, se produzca
inmediatamente, incluso si se produce una excepción. Un ejemplo de esta limpieza es
llamar a Close en un FileStream inmediatamente después de su uso en lugar de esperar
a que el objeto sea recolectado por el Common Language Runtime, de esta forma:
C#
static void CodeWithoutCleanup()
FileStream? file = null;
FileInfo fileInfo = new FileInfo("./file.txt");
file = fileInfo.OpenWrite();
file.WriteByte(0xF);
file.Close();
Ejemplo
Para activar el código anterior en una instrucción try-catch-finally , el código de
limpieza se separa del código de trabajo, de esta forma.
C#
static void CodeWithCleanup()
FileStream? file = null;
FileInfo? fileInfo = null;
try
fileInfo = new FileInfo("./file.txt");
file = fileInfo.OpenWrite();
file.WriteByte(0xF);
catch (UnauthorizedAccessException e)
Console.WriteLine(e.Message);
finally
file?.Close();
Dado que una excepción puede producirse en cualquier momento dentro del bloque
try antes de la llamada a OpenWrite() , o que podría producirse un error en la propia
llamada a OpenWrite() , no se garantiza que el archivo esté abierto cuando se intente
cerrar. El bloque finally agrega una comprobación para asegurarse de que el objeto
FileStream no sea null antes de que se pueda llamar al método Close. Sin la
comprobación null , el bloque finally podría iniciar su propia excepción
NullReferenceException, pero debería evitarse generar excepciones en bloques finally ,
si es posible.
Una conexión de base de datos es otra buena candidata para cerrarse en un bloque
finally . Dado que a veces se limita el número de conexiones permitido en un servidor
de base de datos, se deben cerrar las conexiones de base de datos tan pronto como sea
posible. Si se produce una excepción antes de poder cerrar la conexión, es mejor usar el
bloque finally que esperar a la recolección de elementos no utilizados.
Consulte también
using (instrucción)
try-catch
try-finally
try-catch-finally
Novedades de C# 11
Artículo • 21/02/2023 • Tiempo de lectura: 9 minutos
Se agregaron las siguientes características en C# 11:
Literales de cadena sin formato
Compatibilidad con matemáticas genéricas
Atributos genéricos
Literales de cadena UTF-8
Nuevas líneas en expresiones de interpolación de cadenas
Patrones de lista
Tipos locales de archivo
Miembros requeridos
Structs predeterminados automáticos
Coincidencia de patrones de Span<char> en una string de constante
Ámbito nameof ampliado
IntPtr numérico
Campos ref y scoped ref
Conversión mejorada de grupo de métodos a delegado
Ola de advertencias 7
Puede descargar la versión más reciente de Visual Studio 2022 . También puede probar
todas estas características con el SDK de .NET 7, que se puede descargar desde la
página de descargas de .NET .
7 Nota
Estamos interesados en sus comentarios sobre estas características. Si encuentra
problemas con cualquiera de estas nuevas características, cree un nuevo
problema en el repositorio dotnet/roslyn .
Atributos genéricos
Puede declarar una clase genérica cuya clase base sea System.Attribute. Esta
característica proporciona una sintaxis más cómoda para los atributos que requieren un
parámetro System.Type. Antes había que crear un atributo que tomara Type como
parámetro de constructor:
C#
// Before C# 11:
public class TypeAttribute : Attribute
public TypeAttribute(Type t) => ParamType = t;
public Type ParamType { get; }
Y, para aplicar el atributo, se usa el operador typeof:
C#
[TypeAttribute(typeof(string))]
public string Method() => default;
Con esta nueva característica, puede crear un atributo genérico en su lugar:
C#
// C# 11 feature:
public class GenericAttribute<T> : Attribute { }
Luego, especifique el parámetro de tipo para usar el atributo:
C#
[GenericAttribute<string>()]
public string Method() => default;
Debe proporcionar todos los parámetros de tipo al aplicar el atributo. En otras palabras,
el tipo genérico debe construirse completamente.
En el ejemplo anterior, los paréntesis
vacíos ( ( y ) ) se pueden omitir, ya que el atributo no tiene ningún argumento.
C#
public class GenericType<T>
[GenericAttribute<T>()] // Not allowed! generic attributes must be fully
constructed types.
public string Method() => default;
Los argumentos de tipo deben cumplir las mismas restricciones que el operador typeof.
No se permiten los tipos que requieren anotaciones de metadatos. Por ejemplo, no se
permiten los siguientes tipos como parámetro de tipo:
dynamic
string? (o cualquier tipo de referencia que acepte valores NULL)
(int X, int Y) (o cualquier otro tipo de tupla que use la sintaxis de tupla de C#)
Estos tipos no se representan directamente en los metadatos Incluyen anotaciones que
describen el tipo. En todos los casos, se puede usar el tipo subyacente en su lugar:
object para dynamic
string en lugar de string? .
ValueTuple<int, int> en lugar de (int X, int Y) .
Compatibilidad matemática genérica
Hay varias características de lenguaje que habilitan la compatibilidad matemática
genérica:
static virtual miembros en interfaces
Operador definido por el usuario checked
operadores de desplazamiento relajado
Operador de desplazamiento a la derecha sin signo
Puede agregar static abstract o static virtual miembros en interfaces para definir
interfaces que incluyan operadores sobrecargables, otros miembros estáticos y
propiedades estáticas. El escenario principal de esta característica es usar operadores
matemáticos en tipos genéricos. Por ejemplo, puede implementar la interfaz de
System.IAdditionOperators<TSelf, TOther, TResult> en un tipo que implementa
operator + . Otras interfaces definen otras operaciones matemáticas o valores bien
definidos. Puede obtener información sobre la nueva sintaxis en el artículo sobre
interfaces. Las interfaces que incluyen métodos de static virtual suelen ser interfaces
genéricas. Además, la mayoría declarará una restricción que el parámetro de tipo
implementa en la interfaz declarada.
Para más información y probar la característica usted mismo, consulte el tutorial
Exploración de miembros de la interfaz abstracta estática o la entrada de blog
Características en versión preliminar de .NET 6: matemáticas genéricas .
Las matemáticas genéricas han creado otros requisitos en el lenguaje.
Operador de desplazamiento a la derecha sin signo: antes de C# 11, para forzar un
desplazamiento a la derecha sin signo, había que convertir cualquier tipo entero
con signo en un tipo sin signo, llevar a cabo el desplazamiento y, después,
convertir el resultado en un tipo con signo. A partir de C# 11, puede usar >>> , el
operador de desplazamiento sin signo.
Requisitos de operador de desplazamiento menos estrictos: C# 11 elimina el
requisito de que el segundo operando debe ser un int o convertible
implícitamente en int . Este cambio permite que los tipos que implementan
interfaces matemáticas genéricas se usen en estas ubicaciones.
Operadores definidos por el usuario checked y unchecked: los desarrolladores ya
pueden definir los operadores aritméticos checked y unchecked . El compilador
genera llamadas a la variante correcta en función del contexto actual. Puede
obtener más información sobre los checked operadores en el artículo sobre
operadores aritméticos.
IntPtr y UIntPtr numéricos
Los tipos nint y nuint ahora se conocen como System.IntPtr y System.UIntPtr
respectivamente.
Nuevas líneas en interpolaciones de cadenas
El texto dentro de los caracteres { y } de una interpolación de cadenas ahora puede
abarcar varias líneas. El texto entre los marcadores { y } se analiza como C#. Se
permite cualquier C# válido, incluidas las nuevas líneas. Esta característica facilita la
lectura de interpolaciones de cadenas que usan expresiones de C# más largas, como
expresiones switch de coincidencia de patrones o consultas LINQ.
Puede aprender más sobre la característica de nuevas líneas en el artículo
Interpolaciones de cadenas de la referencia del lenguaje.
Patrones de lista
Los patrones de lista amplían la coincidencia de patrones para buscar coincidencias con
secuencias de elementos de una lista o una matriz. Por ejemplo, sequence is [1, 2, 3]
es true cuando sequence es una matriz o una lista de tres enteros (1, 2 y 3). Puede
hacer coincidir elementos mediante cualquier patrón, como constante, tipo, propiedad y
patrones relacionales. El patrón de descarte ( _ ) coincide con cualquier elemento único y
el nuevo patrón de intervalo ( .. ) coincide con cualquier secuencia de cero o más
elementos.
Puede encontrar más información sobre los patrones de lista en el artículo sobre
coincidencia de patrones en la referencia del lenguaje.
Conversión mejorada de grupo de métodos a
delegado
El estándar de C# en conversiones de grupo de métodos ahora incluye el siguiente
elemento:
La conversión se permite (pero no necesaria) para usar una instancia de
delegado existente que ya contiene estas referencias.
Las versiones anteriores del estándar prohibían al compilador reutilizar el objeto
delegado creado para una conversión de grupo de métodos. El compilador de C# 11
almacena en caché el objeto delegado creado a partir de una conversión de grupo de
métodos y reutiliza ese objeto delegado único. Esta característica se incluyó por primera
vez en Visual Studio 2022 versión 17.2 como característica en vista previa y en .NET 7
(versión preliminar 2).
Literales de cadena sin formato
Los literales de cadena sin formato son un nuevo formato para los literales de cadena.
Los literales de cadena sin formato pueden contener texto arbitrario, como espacios en
blanco, nuevas líneas, comillas insertadas y otros caracteres especiales sin necesidad de
secuencias de escape. Un literal de cadena sin formato comienza con al menos tres
caracteres de comillas dobles ("""). Y termina con el mismo número de caracteres de
comillas dobles. Normalmente, un literal de cadena sin formato usa tres comillas dobles
en una sola línea para iniciar la cadena y tres comillas dobles en una línea independiente
para finalizar la cadena. Las nuevas líneas que siguen a la comilla de apertura y
preceden a la comilla de cierre no se incluyen en el contenido final:
C#
string longMessage = """
This is a long message.
It has several lines.
Some are indented
more than others.
Some should start at the first column.
Some have "quoted text" in them.
""";
Cualquier espacio en blanco a la izquierda de las comillas dobles de cierre se quitará del
literal de cadena. Los literales de cadena sin formato se pueden combinar con la
interpolación de cadenas para incluir llaves en el texto de salida. Varios caracteres $
indican cuántas llaves consecutivas comienzan y terminan la interpolación:
C#
var location = $$"""
You are at {{{Longitude}}, {{Latitude}}}
""";
En el ejemplo anterior se especifica que dos llaves inician y finalizan una interpolación.
La tercera llave de apertura y cierre repetida se incluye en la cadena de salida.
Puede encontrar más información sobre los literales de cadena sin formato en el artículo
sobre cadenas de la guía de programación y los artículos de referencia del lenguaje
sobre literales de cadena y cadenas interpoladas.
Estructuras predeterminadas automáticas
El compilador de C# 11 garantiza que todos los campos de un tipo struct se
inicializarán en su valor predeterminado como parte de la ejecución de un constructor.
Este cambio significa que el compilador inicializa automáticamente cualquier campo o
propiedad automática no inicializados por un constructor. Las estructuras en las que el
constructor no asigna definitivamente todos los campos ahora se compilan, mientras
que los campos que no se inicializan explícitamente se establecen en su valor
predeterminado. Puede obtener más información sobre cómo afecta este cambio a la
inicialización de estructuras en el artículo sobre estructuras.
Span<char> de coincidencia de patrón o
ReadOnlySpan<char> en una constante string
Ha podido probar si un elemento string tiene un valor constante específico mediante
la coincidencia de patrones en varias versiones. Ahora, puede usar la misma lógica de
coincidencia de patrones con variables que son Span<char> o ReadOnlySpan<char> .
Ámbito nameof ampliado
Los nombres de parámetro de tipo y los de parámetro ahora están en el ámbito cuando
se usan en una expresión nameof de una declaración de atributo del método. Esta
característica significa que puede usar el operador nameof para especificar el nombre de
un parámetro de método en un atributo de una declaración de método o parámetro.
Esta característica suele ser útil para agregar atributos y llevar a cabo análisis que
admitan valores NULL.
Literales de cadena de UTF-8
Puede especificar el sufijo u8 en un literal de cadena para especificar la codificación de
caracteres UTF-8. Si la aplicación necesita cadenas UTF-8, para constantes de cadena
HTTP o protocolos de texto similares, puede usar esta característica para simplificar la
creación de cadenas UTF-8.
Puede obtener más información sobre los literales de cadena UTF-8 en la sección literal
de cadena del artículo sobre los tipos de referencia integrados.
Miembros requeridos
Puede agregar el modificadorrequired a propiedades y campos para aplicar
constructores y llamadores para inicializar esos valores. El
System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute se puede agregar a
constructores para informar al compilador de que un constructor inicializa todos los
miembros necesarios.
Para obtener más información sobre los miembros necesarios, consulte la sección solo
inicial del artículo de propiedades.
Campos ref y variables ref scoped
Puede declarar campos ref en ref struct. Esto admite el uso de tipos como
System.Span<T> sin atributos especiales o tipos internos ocultos.
Puede agregar el modificador scoped a cualquier declaración de ref . Esto limita el
ámbito al que la referencia puede escapar.
Tipos locales de archivo
A partir de C# 11, el modificador de acceso file se puede usar para crear un tipo cuya
visibilidad esté limitada al archivo de origen en el que se declara. Esta característica
ayuda a los autores de generadores de código fuente a evitar conflictos de
nomenclatura. Puede obtener más información sobre esta característica en el artículo
sobre tipos con ámbito de archivo en la sección de referencia del lenguaje.
Vea también
Novedades de .NET 7
Novedades de C# 10
Artículo • 21/02/2023 • Tiempo de lectura: 6 minutos
C# 10 agrega las características y las mejoras al lenguaje C# siguientes:
Structs de registro
Mejoras de tipos de estructura
Controladores de cadena interpolada
Directivas global using
Declaración de espacios de nombres con ámbito de archivo
Patrones de propiedades extendidos
Mejoras en expresiones lambda
Se permiten cadenas interpoladas const
Los tipos de registro pueden sellar ToString()
Asignación definitiva mejorada
Se permite la asignación y la declaración en la misma desconstrucción
Se permite el atributo AsyncMethodBuilder en los métodos
Atributo CallerArgumentExpression
Pragma #line mejorado
Ola de advertencias 6
C# 10 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 más reciente de la página de descargas de .NET .
También puede descargar Visual Studio 2022 , que incluye el SDK de .NET 6.
7 Nota
Estamos interesados en sus comentarios sobre estas características. Si encuentra
problemas con cualquiera de estas nuevas características, cree un nuevo
problema en el repositorio dotnet/roslyn .
Structs de registro
Puede declarar los registros de tipo de valor mediante las declaraciones record struct o
readonly record struct. Ahora puede aclarar que un elemento record es un tipo de
referencia con la declaración record class .
Mejoras de tipos de estructura
C# 10 presenta las mejoras siguientes relacionadas con los tipos de estructura:
Puede declarar un constructor sin parámetros de instancia en un tipo de estructura
e inicializar un campo o propiedad de instancia en su declaración. Para más
información, consulte la sección Inicialización de estructuras y valores
predeterminados del artículo Tipos de estructuras.
Un operando izquierdo de la expresión with puede ser de cualquier tipo de
estructura o un tipo anónimo (de referencia).
Controlador de cadena interpolada
Puede crear un tipo que compile la cadena resultante a partir de una expresión de
cadena interpolada. Las bibliotecas de .NET usan esta característica en muchas API.
Puede compilar una siguiendo este tutorial.
Directivas using globales
Puede agregar el modificador global a cualquier directiva using para indicar al
compilador que la directiva se aplica a todos los archivos de código fuente de la
compilación. Normalmente, se trata de todos los archivos de código fuente de un
proyecto.
Declaración de espacios de nombres con
ámbito de archivo
Puede usar una nueva forma de la declaración namespace para declarar que todas las
declaraciones posteriores son miembros del espacio de nombres declarado:
C#
namespace MyNamespace;
Esta nueva sintaxis ahorra espacio horizontal y vertical para las declaraciones namespace .
Patrones de propiedades extendidos
A partir de C# 10, puede hacer referencia a propiedades o campos anidados en un
patrón de propiedad. Por ejemplo, un patrón con el formato
C#
{ Prop1.Prop2: pattern }
es válido en C# 10 y versiones posteriores, y equivalente a
C#
{ Prop1: { Prop2: pattern } }
válido en C# 8.0 y versiones posteriores.
Para obtener más información, consulte la nota de propuesta de características Patrones
de propiedades extendidos. Para obtener más información sobre un patrón de
propiedades, vea la sección Patrón de propiedad del artículo Patrones.
Mejoras de expresiones lambda
C# 10 incluye muchas mejoras en el modo en que se controlan las expresiones lambda:
Las expresiones lambda pueden tener un tipo natural, donde el compilador puede
deducir un tipo delegado de la expresión lambda o del grupo de métodos.
Las expresiones lambda pueden declarar un tipo de valor devuelto cuando el
compilador no puede deducirlo.
Los atributos se pueden aplicar a las expresiones lambda.
Estas características hacen que las expresiones lambda sean parecidas a los métodos y
las funciones locales. Facilitan el uso de expresiones lambda sin declarar una variable de
un tipo delegado y funcionan de forma más fluida con las nuevas API mínimas de
ASP.NET CORE.
Cadenas interpoladas constantes
En C# 10, las cadenas const se pueden inicializar mediante la interpolación de cadenas
si todos los marcadores de posición son cadenas constantes. La interpolación de
cadenas puede crear cadenas constantes más legibles a medida que se compilan las
cadenas constantes usadas en la aplicación. Las expresiones de marcador de posición no
pueden ser constantes numéricas porque esas constantes se convierten en cadenas en
tiempo de ejecución. La referencia cultural actual puede afectar a su representación de
cadena. Obtenga más información en la referencia del lenguaje sobre expresiones const.
Los tipos de registro pueden sellar ToString
En C# 10, puede agregar el modificador sealed al invalidar ToString en un tipo de
registro. Al sellar el método ToString , se impide que el compilador sintetice un método
ToString para cualquier tipo de registro derivado. Un elemento ToString sealed
(sellado) garantiza que todos los tipos de registros derivados usen el método ToString ,
definido en un tipo de registro base común. Puede obtener más información sobre esta
característica en el artículo sobre registros.
Asignación y declaración en la misma
desconstrucción
Este cambio quita una restricción de versiones anteriores de C#. Anteriormente, una
desconstrucción podía asignar todos los valores a variables existentes, o bien inicializar
variables recién declaradas:
C#
// Initialization:
(int x, int y) = point;
// assignment:
int x1 = 0;
int y1 = 0;
(x1, y1) = point;
En C# 10 se elimina esta restricción:
C#
int x = 0;
(x, int y) = point;
Asignación definitiva mejorada
Antes de C# 10, había muchos escenarios en los que la asignación definitiva y el análisis
de estado NULL generaban advertencias que eran falsos positivos. Por lo general, estas
implicaban comparaciones con constantes booleanas, el acceso a una variable solo en
las instrucciones true o false de una instrucción if , y expresiones de uso combinado
de NULL. Estos ejemplos generaron advertencias en versiones anteriores de C#, pero no
en C# 10:
C#
string representation = "N/A";
if ((c != null && c.GetDependentValue(out object obj)) == true)
representation = obj.ToString(); // undesired error
// Or, using ?.
if (c?.GetDependentValue(out object obj) == true)
representation = obj.ToString(); // undesired error
// Or, using ??
if (c?.GetDependentValue(out object obj) ?? false)
representation = obj.ToString(); // undesired error
El impacto principal de esta mejora es que las advertencias para la asignación definitiva
y el análisis de estado NULL son más precisas.
Se permite el atributo AsyncMethodBuilder en
los métodos
En C# 10 y versiones posteriores, puede especificar un generador de métodos
asincrónicos diferente para un único método, además de especificar el tipo de
generador de métodos para todos los métodos que devuelven un tipo concreto similar
a una tarea. Un generador de métodos asíncronos personalizado permite escenarios
avanzados de optimización del rendimiento en los que un método determinado puede
beneficiarse de un generador personalizado.
Para obtener más información, vea la sección sobre AsyncMethodBuilder del artículo
sobre los atributos leídos por el compilador.
Diagnóstico del atributo
CallerArgumentExpression
Puede usar System.Runtime.CompilerServices.CallerArgumentExpressionAttribute para
especificar un parámetro que el compilador reemplace por la representación de texto de
otro argumento. Esta característica permite a las bibliotecas crear diagnósticos más
específicos. El código siguiente prueba una condición. Si la condición es false, el
mensaje de excepción incluye la representación de texto del argumento pasado a
condition :
C#
public static void Validate(bool condition,
[CallerArgumentExpression("condition")] string? message=null)
if (!condition)
throw new InvalidOperationException($"Argument failed validation:
<{message}>");
Puede obtener más información sobre esta característica en el artículo sobre Atributos
de información del autor de la llamada en la sección de referencia del lenguaje.
Pragma #line mejorado
C# 10 admite un formato nuevo para el pragma #line . Es probable que no use el
formato nuevo, pero verá sus efectos. Las mejoras permiten una salida más detallada en
lenguajes específicos de dominio (DSL), como Razor. El motor de Razor usa estas
mejoras para mejorar la experiencia de depuración. Encontrará que los depuradores
pueden resaltar el origen de Razor con más precisión. Para obtener más información
sobre la nueva sintaxis, vea el artículo sobre Directivas de preprocesador en la referencia
del lenguaje. También puede leer la especificación de la característica para obtener
ejemplos basados en Razor.
Novedades de C# 9.0
Artículo • 15/02/2023 • Tiempo de lectura: 20 minutos
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
Ola de advertencias 5
C# 9.0 es compatible con .NET 5. Para obtener más información, vea Control de
versiones del lenguaje C#.
Puede descargar el SDK de .NET más reciente de la página de descargas de .NET .
Tipos de registro
C# 9.0 introduce 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:
C#
public record Person(string FirstName, string LastName);
C#
public record Person
public string FirstName { get; init; } = default!;
public string LastName { get; init; } = default!;
};
También puede crear tipos de registros con propiedades y campos mutables:
C#
public record Person
public string FirstName { get; set; } = default!;
public string LastName { get; set; } = default!;
};
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:
C#
public record Person(string FirstName, string LastName);
public static void Main()
Person person = new("Nancy", "Davolio");
Console.WriteLine(person);
// output: Person { FirstName = Nancy, LastName = Davolio }
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:
C#
public record Person(string FirstName, string LastName, string[]
PhoneNumbers);
public static void Main()
var phoneNumbers = new string[2];
Person person1 = new("Nancy", "Davolio", phoneNumbers);
Person person2 = new("Nancy", "Davolio", phoneNumbers);
Console.WriteLine(person1 == person2); // output: True
person1.PhoneNumbers[0] = "555-1234";
Console.WriteLine(person1 == person2); // output: True
Console.WriteLine(ReferenceEquals(person1, person2)); // output: False
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:
C#
public record Person(string FirstName, string LastName)
public string[] PhoneNumbers { get; init; }
public static void Main()
Person person1 = new("Nancy", "Davolio") { PhoneNumbers = new string[1]
};
Console.WriteLine(person1);
// output: Person { FirstName = Nancy, LastName = Davolio, PhoneNumbers
= System.String[] }
Person person2 = person1 with { FirstName = "John" };
Console.WriteLine(person2);
// output: Person { FirstName = John, LastName = Davolio, PhoneNumbers =
System.String[] }
Console.WriteLine(person1 == person2); // output: False
person2 = person1 with { PhoneNumbers = new string[1] };
Console.WriteLine(person2);
// output: Person { FirstName = Nancy, LastName = Davolio, PhoneNumbers
= System.String[] }
Console.WriteLine(person1 == person2); // output: False
person2 = person1 with { };
Console.WriteLine(person1 == person2); // output: True
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:
<nombre de tipo de registro> { <nombre de propiedad> = <value>, <nombre de
propiedad> = <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 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:
C#
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:
C#
public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public record Student(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public static void Main()
Person teacher = new Teacher("Nancy", "Davolio", 3);
Person student = new Student("Nancy", "Davolio", 3);
Console.WriteLine(teacher == student); // output: False
Student student2 = new Student("Nancy", "Davolio", 3);
Console.WriteLine(student2 == student); // output: True
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:
C#
public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public record Student(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 obtener más información, vea Herencia en el artículo de referencia del lenguaje C#
acerca de los registros.
Establecedores de solo inicialización
Los establecedores de solo inicialización proporcionan una sintaxis coherente para
inicializar los miembros de un objeto. Los inicializadores de propiedades indican con
claridad qué valor establece cada propiedad. El inconveniente es que esas propiedades
se deben establecer. A partir de C# 9.0, puede crear descriptores de acceso init en
lugar de descriptores de acceso set para propiedades e indizadores. Los autores de la
llamada pueden usar la sintaxis de inicializador de propiedad para establecer estos
valores en expresiones de creación, pero esas propiedades son de solo lectura una vez
que se ha completado la construcción. Los establecedores de solo inicialización
proporcionan una ventana para cambiar el estado, que se cierra cuando finaliza la fase
de construcción. La fase de construcción finaliza de forma eficaz después de que se
complete toda la inicialización, incluidos los inicializadores de propiedades y las
expresiones with.
Puede declarar los establecedores de solo init en cualquier tipo que escriba. Por
ejemplo, en la estructura siguiente se define una estructura de observación
meteorológica:
C#
public struct WeatherObservation
public DateTime RecordedAt { get; init; }
public decimal TemperatureInCelsius { get; init; }
public decimal PressureInMillibars { get; init; }
public override string ToString() =>
$"At {RecordedAt:h:mm tt} on {RecordedAt:M/d/yyyy}: " +
$"Temp = {TemperatureInCelsius}, with {PressureInMillibars}
pressure";
Los autores de la llamada pueden usar la sintaxis de inicializador de propiedades para
establecer los valores, a la vez que conservan la inmutabilidad:
C#
var now = new WeatherObservation
RecordedAt = DateTime.Now,
TemperatureInCelsius = 20,
PressureInMillibars = 998.0m
};
Un intento de cambiar una observación después de la inicialización genera un error del
compilador:
C#
// 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.
Para obtener más información, vea init (referencia de C#).
Instrucciones de nivel superior
Las instrucciones de nivel superior eliminan la complejidad innecesaria de muchas
aplicaciones. Considere el programa canónico "Hola mundo":
C#
using System;
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:
C#
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:
C#
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 de C# principiantes pueden escribir el programa
canónico "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#.
Mejoras de coincidencia de patrones
C# 9 incluye nuevas mejoras de coincidencia de patrones:
Los patrones de tipo coinciden con un objeto que coincide con un tipo
determinado.
Los patrones con paréntesis aplican o resaltan la prioridad de las combinaciones
de patrones
En los patrones and conjuntivos es necesario que los dos patrones coincidan.
En los patrones or disyuntivos es necesario que alguno de los patrones coincida
En los patrones not negados es necesario que un patrón no coincida.
Los patrones relacionales requieren que la entrada sea menor que, mayor que,
menor o igual que, o mayor o igual que una constante determinada.
Estos patrones enriquecen la sintaxis de los patrones. Tenga en cuenta estos ejemplos:
C#
public static bool IsLetter(this char c) =>
c is >= 'a' and <= 'z' or >= 'A' and <= 'Z';
Con paréntesis opcionales para que quede claro que and tiene mayor prioridad que or :
C#
public static bool IsLetterOrSeparator(this char c) =>
c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z') or '.' or ',';
Uno de los usos más comunes es una nueva sintaxis para una comprobación NULL:
C#
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 .
Para obtener más información, consulte Patrones (referencia de C#).
Para obtener más información, vea las secciones Patrones relacionales y Patrones lógicos
del artículo Patrones.
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 tiempo 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.
Características de ajuste y finalización
Muchas de las características restantes ayudan a escribir código de forma más eficaz. En
C# 9.0, puede omitir el tipo de una expresión new cuando ya se conoce el tipo del
objeto creado. El uso más común es en las declaraciones de campo:
C#
private List<WeatherObservation> _observations = new();
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:
C#
public WeatherForecast ForecastFor(DateTime forecastDate,
WeatherForecastOptions options)
Podría llamarlo de esta forma:
C#
var forecast = station.ForecastFor(DateTime.Now.AddDays(2), new());
Otra aplicación muy útil de esta característica es para combinarla con propiedades de
solo inicialización para inicializar un objeto nuevo:
C#
WeatherStation station = new() { Location = "Seattle, WA" };
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.
Compatibilidad con generadores de código
Dos últimas características admiten generadores de código de C#. Los generadores de
código de C# son un componente que se puede escribir y que es similar a una
corrección de código o un analizador Roslyn. La diferencia es que los generadores de
código analizan el código y escriben nuevos archivos de código fuente como parte del
proceso de compilación. Un generador de código típico busca atributos y otras
convenciones en el código.
Un generador de código lee atributos u otros elementos de código mediante las API de
análisis de Roslyn. A partir de esa información, agrega código nuevo a la compilación.
Los generadores de código fuente solo pueden agregar código; no se les permite
modificar ningún código existente en la compilación.
Las dos características agregadas a los generadores de código son extensiones de la
sintaxis de método parcial y los inicializadores de módulos. En primer lugar, los
cambios en los métodos parciales. Antes de C# 9.0, los métodos parciales eran private ,
pero no podían especificar un modificador de acceso, tener un valor devuelto void ni
parámetros out . Estas restricciones implican que si no se proporciona ninguna
implementación de método, el compilador quita todas las llamadas al método parcial.
En C# 9.0 se quitan estas restricciones, pero es necesario que las declaraciones de
métodos parciales tengan una implementación. Los generadores de código pueden
proporcionar esa implementación. Para evitar la introducción de un cambio importante,
el compilador tiene en cuenta cualquier método parcial sin un modificador de acceso
para seguir las reglas anteriores. Si el método parcial incluye el modificador de acceso
private , las nuevas reglas rigen ese método parcial. Para obtener más información, vea
partial (Método) (referencia de C#).
La segunda característica nueva de los generadores de código son los inicializadores de
módulos. Los inicializadores de módulos son métodos que tienen asociado el atributo
ModuleInitializerAttribute. El entorno de ejecución llamará a estos métodos antes de
cualquier otro acceso de campo o invocación de método en todo el módulo. Un
método de inicializador de módulo:
Debe ser estático
No debe tener parámetros
Debe devolver void
No debe ser un método genérico
No debe estar incluido en una clase genérica
Debe ser accesible desde el módulo contenedor
Ese último punto significa en realidad que el método y su clase contenedora deben ser
internos o públicos. El método no puede ser una función local. Para obtener más
información, vea Atributo ModuleInitializer.
Historia de C#
Artículo • 09/03/2023 • Tiempo de lectura: 17 minutos
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.
) Importante
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 11
Fecha de publicación noviembre de 2022
Se agregaron las siguientes características en C# 11:
Literales de cadena sin formato
Compatibilidad con matemáticas genéricas
Atributos genéricos
Literales de cadena UTF-8
Nuevas líneas en expresiones de interpolación de cadenas
Patrones de lista
Tipos locales de archivo
Miembros requeridos
Structs predeterminados automáticos
Coincidencia de patrones de Span<char> en una string de constante
Ámbito nameof ampliado
IntPtr numérico
Campos ref y scoped ref
Conversión mejorada de grupo de métodos a delegado
Ola de advertencias 7
C# 11 presenta matemáticas genéricas y varias características que admiten ese objetivo.
Puede escribir algoritmos numéricos una vez para todos los tipos de números. Hay más
características para facilitar el trabajo con struct tipos, como los miembros necesarios y
las estructuras predeterminadas automáticas. Trabajar con cadenas resulta más fácil con
literales de cadena sin formato, nueva línea en interpolaciones