Capítulo 1. Introducción a C#

Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com

El lenguaje de programación C# (pronunciado "see sharp") se utiliza para muchos tipos de aplicaciones, como sitios web, sistemas basados en la nube, inteligencia artificial, dispositivos IoT, aplicaciones de escritorio, controladores integrados, aplicaciones móviles, juegos y utilidades de línea de comandos. C#, junto con el tiempo de ejecución de apoyo, las bibliotecas y las herramientas conocidas colectivamente como .NET, ha sido el centro de atención de los desarrolladores de Windows durante más de 20 años. Hoy en día, .NET es multiplataforma y de código abierto, lo que permite que las aplicaciones y servicios escritos en C# se ejecuten en sistemas operativos como Android, iOS, macOS y Linux, además de en Windows.

Cada nueva versión de C# ha mejorado la productividad de los desarrolladores. Por ejemplo, las versiones más recientes incluyen nuevas funciones de concordancia de patrones para que nuestro código sea más expresivo y sucinto. Los constructores primarios y las expresiones de colección ayudan a reducir la verbosidad en algunos escenarios comunes. Varias mejoras del sistema de tipos permiten que nuestro código exprese sus requisitos y características con más detalle, lo que nos permite escribir bibliotecas más flexibles y disfrutar de mejores diagnósticos en tiempo de compilación.

En C# 11.0 y 12.0 se han incorporado funciones orientadas al rendimiento, como las matemáticas genéricas, y se ha mejorado el control de la gestión de la memoria para el código de bajo nivel sensible al rendimiento. Cada nueva versión de .NET ha mejorado la velocidad de ejecución, pero también se han producido reducciones significativas en los tiempos de inicio, la huella de memoria y el tamaño del binario. Esto, junto con la compatibilidad mejorada con la contenedorización, mejora la adecuación de .NET al desarrollo moderno en la nube. También ha habido mejoras significativas para el desarrollo multiplataforma del lado del cliente, gracias a Blazor y .NET MAUI (Multi-platform App UI). .NET ha soportado ARM y WebAssembly (WASM) durante muchos años, pero las continuas mejoras recientes para esos objetivos son importantes para el desarrollo en la nube, móvil y web.

C# y .NET son proyectos de código abierto, aunque no empezaron así. En los primeros tiempos de C#, Microsoft guardaba celosamente todo su código fuente; sin embargo, en 2014, se creó la Fundación .NET para fomentar el desarrollo de proyectos de código abierto en el mundo .NET. Muchos de los proyectos C# y .NET más importantes de Microsoft están ahora bajo el gobierno de la fundación (además de muchos proyectos ajenos a Microsoft). Esto incluye el estándar del lenguaje C#, el compilador C# de Microsoft y también el tiempo de ejecución y las bibliotecas .NET. En la actualidad, casi todo lo que rodea a C# se desarrolla en abierto, y las contribuciones de código ajenas a Microsoft son bienvenidas. Las propuestas de nuevas características del lenguaje se gestionan en GitHub, lo que permite la participación de la comunidad desde las primeras fases.

¿Por qué C#?

Aunque hay muchas formas de utilizar C#, otros lenguajes son siempre una opción. ¿Por qué podrías elegir C# en lugar de otros? Dependerá de lo que necesites hacer y de lo que te guste y disguste en un lenguaje de programación. A mí me parece que C# proporciona una potencia, flexibilidad y rendimiento considerables y funciona a un nivel de abstracción lo suficientemente alto como para que no tenga que dedicar grandes cantidades de esfuerzo a pequeños detalles no relacionados directamente con los problemas que mis programas intentan resolver.

Gran parte de la potencia de C# procede del abanico de técnicas de programación que admite. Por ejemplo, ofrece funciones orientadas a objetos, genéricos y programación funcional. Admite tipado dinámico y estático. Ofrece potentes funciones orientadas a listas y conjuntos, gracias a la consulta integrada en el lenguaje (LINQ). Tiene soporte intrínseco para la programación asíncrona. Además, los distintos entornos de desarrollo compatibles con C# ofrecen una amplia gama de funciones que mejoran la productividad.

C# ofrece opciones para equilibrar la facilidad de desarrollo con el rendimiento. El tiempo de ejecución siempre ha proporcionado un recolector de basura (CG) que libera a los desarrolladores de gran parte del trabajo asociado a la recuperación de memoria que el programa ya no utiliza. Un GC es una característica común en los lenguajes de programación modernos, y aunque es una bendición para la mayoría de los programas, hay algunos escenarios especializados en los que sus implicaciones para el rendimiento son problemáticas. Por eso C# también permite una gestión más explícita de la memoria, dándote la opción de cambiar la facilidad de desarrollo por el rendimiento en tiempo de ejecución, pero sin perder la seguridad de tipos. Esto hace que C# sea adecuado para ciertas aplicaciones de rendimiento crítico que durante años fueron el coto privado de lenguajes menos seguros como C y C++.

Los lenguajes no existen en el vacío: las bibliotecas de alta calidad con una amplia gama de funciones son esenciales. Algunos lenguajes elegantes y académicamente bellos son gloriosos hasta que quieres hacer algo prosaico, como hablar con una base de datos o determinar dónde almacenar la configuración del usuario. Por muy potente que sea el conjunto de modismos de programación que ofrezca un lenguaje, también tiene que proporcionar un acceso completo y cómodo a los servicios de la plataforma subyacente. C# se encuentra en un terreno muy sólido en este sentido, gracias a su tiempo de ejecución, las bibliotecas de clases incorporadas y la amplia compatibilidad con bibliotecas de terceros.

.NET engloba tanto el tiempo de ejecución como las bibliotecas de clases principales que utilizan los programas C#. La parte del tiempo de ejecución se denomina Common Language Runtime (normalmente abreviado CLR) porque no sólo admite C#, sino cualquier lenguaje .NET. Microsoft también ofrece extensiones de Visual Basic, F# y .NET para C++, por ejemplo. El CLR tiene un Sistema de Tipos Comunes (CTS) que permite que el código de varios lenguajes interopere libremente, lo que significa que las bibliotecas .NET pueden utilizarse normalmente desde cualquier lenguaje .NET: F# puede consumir bibliotecas escritas en C#, C# puede utilizar bibliotecas de Visual Basic, etc.

.NET incluye un amplio conjunto de bibliotecas de clases. Han recibido varios nombres a lo largo de los años, como Biblioteca de clases base (BCL), Biblioteca de clases del marco de trabajo y bibliotecas del marco de trabajo, pero Microsoft parece haberse decantado ahora por las bibliotecas en tiempo de ejecución como nombre para esta parte de .NET. Estas bibliotecas proporcionan envoltorios para muchas características del sistema operativo (SO) subyacente, pero también proporcionan una cantidad considerable de funcionalidad propia, como clases de colección y procesamiento JSON.

Las bibliotecas de clases en tiempo de ejecución .NET no lo son todo: muchos otros sistemas proporcionan sus propias bibliotecas .NET. Por ejemplo, hay bibliotecas que permiten a los programas C# utilizar servicios populares en la nube. Como era de esperar, Microsoft proporciona completas bibliotecas .NET para trabajar con los servicios de su plataforma en la nube Azure. Del mismo modo, Amazon proporciona un completo kit de desarrollo para utilizar Amazon Web Services (AWS) desde C# y otros lenguajes .NET. Y las bibliotecas no tienen por qué estar asociadas a servicios concretos. Existe un gran ecosistema de bibliotecas .NET, algunas comerciales y otras gratuitas, que incluyen utilidades matemáticas, bibliotecas de análisis sintáctico y componentes de interfaz de usuario (UI), por nombrar sólo algunas. Incluso si tienes mala suerte y necesitas utilizar una función del sistema operativo que no tenga ninguna envoltura de bibliotecas .NET, C# ofrece varios mecanismos para trabajar con otros tipos de API, como las API de estilo C disponibles en Win32, macOS y Linux, o las API basadas en el Modelo de Objetos Componentes (COM) en Windows.

Además de las bibliotecas, también hay numerosos marcos de aplicaciones. .NET tiene marcos integrados para crear aplicaciones web y API web, aplicaciones de escritorio y aplicaciones móviles. También hay marcos de código abierto para diversos estilos de desarrollo de sistemas distribuidos, como el procesamiento de eventos de gran volumen con Reaqtor o los sistemas distribuidos globalmente de alta disponibilidad con Orleans.

Por último, dado que .NET existe desde hace más de dos décadas, muchas organizaciones han realizado grandes inversiones en tecnología basada en esta plataforma. Así que C# es a menudo la elección natural para recoger los frutos de estas inversiones.

En resumen, C# nos proporciona un sólido conjunto de abstracciones integradas en el lenguaje, unpotente tiempo de ejecución y un fácil acceso a una enorme cantidad de bibliotecas y funcionalidades de la plataforma.

Código gestionado y el CLR

C# fue el primer lenguaje diseñado para ser nativo en el mundo del CLR. Esto da a C# una sensación distintiva. También significa que si quieres entender C#, tienes que entender el CLR y la forma en que ejecuta el código.

Durante años, la forma más habitual de trabajar de un compilador era procesar el código fuente y producir la salida en una forma que pudiera ser ejecutada directamente por la CPU del ordenador. Los compiladores producían código máquina: unaserie de instrucciones en el formato binario que requiriera el tipo de CPU del ordenador. A veces se denomina código nativo, porque es el lenguaje que la CPU entiende intrínsecamente. Muchos compiladores siguen funcionando así, pero aunque podemos compilar C# en código máquina, a menudo no lo hacemos. Esto es opcional porque C# utiliza un modelo llamado código administrado.

Con el código gestionado, el compilador no genera el código máquina que ejecuta la CPU. En su lugar, el compilador produce una forma de código binario llamado lenguaje intermedio (IL). El binario ejecutable se produce más tarde, normalmente, aunque no siempre, en tiempo de ejecución. El uso del IL permite funciones que son difíciles o incluso imposibles de proporcionar con el modelo más tradicional.

Quizás la ventaja más visible del modelo gestionado es que el resultado del compilador no está ligado a una única arquitectura de CPU. Por ejemplo, las CPU de Intel y AMD utilizadas en muchos ordenadores modernos admiten conjuntos de instrucciones de 32 y 64 bits (conocidos, respectivamente, por razones históricas como x86 y x64). Con el antiguo modelo de compilación de código fuente en lenguaje máquina, tendrías que elegir cuál de ellos soportar, creando varias versiones de tu componente si necesitas dirigirte a más de uno. Pero con .NET, puedes crear un único componente que se ejecute sin modificaciones en procesos de 32 o 64 bits. El mismo componente podría incluso ejecutarse en arquitecturas completamente distintas, como ARM (una arquitectura de procesador muy utilizada en teléfonos móviles, Macs con Apple Silicon y también en dispositivos diminutos como la Raspberry Pi). Con un lenguaje que compilara directamente a código máquina, tendrías que construir binarios diferentes para cada una de ellas, o en algunos casos podrías construir un único archivo que contuviera varias copias del código, una para cada arquitectura soportada. Con .NET, puedes compilar un único componente que contenga una sola versión del código, y podrá ejecutarse en cualquiera de ellas. Incluso podría ejecutarse en plataformas que no eran compatibles en el momento en que compilaste el código, si en el futuro estuviera disponible un tiempo de ejecución adecuado. (Por ejemplo, los componentes .NET escritos años antes de que Apple lanzara sus primeros Mac basados en ARM pueden ejecutarse sin depender de la tecnología de traducción Rosetta que normalmente permite que el código más antiguo funcione en losprocesadores más nuevos).

En términos más generales, cualquier tipo de mejora en la generación de código del CLR -ya sea soporte para nuevas arquitecturas de CPU o simplemente mejoras de rendimiento para las existentes- beneficia instantáneamente a todos los lenguajes .NET. Por ejemplo, las versiones anteriores del CLR no aprovechaban las extensiones de procesamiento vectorial disponibles en los procesadores modernos, pero ahora las versiones actuales suelen aprovecharlas algenerar código para bucles. Todo el código que se ejecuta en las versiones actuales de .NET se beneficia de ello, incluidos los componentes que se compilaron años antes de que se añadiera esta mejora.

El momento exacto en que el CLR genera código máquina ejecutable puede variar. Por defecto, utiliza un método llamado compilación justo a tiempo (JIT), en el que el código máquina de cada función individual se genera la primera vez que se ejecuta. Sin embargo, no tiene por qué funcionar así. Una de las implementaciones en tiempo de ejecución, llamada Mono, es capaz de interpretar IL directamente sin convertirlo nunca en lenguaje máquina ejecutable, lo que resulta útil en plataformas como iOS, donde las restricciones legales pueden impedir la compilación JIT. El Kit de Desarrollo de Software (SDK) de .NET también proporciona formas de construir código precompilado junto con el IL. Esta compilaciónpor adelantado (AOT) puede mejorar el tiempo de inicio de una aplicación.

Nota

La generación de código ejecutable puede seguir ocurriendo en tiempo de ejecución. La función de compilación por niveles del CLR puede optar por recompilar un método dinámicamente para optimizarlo mejor para las formas en que se utiliza en tiempo de ejecución, y puede hacerlo incluso cuando utilizas la compilación AOT porque el IL sigue estando disponible en tiempo de ejecución.

El SDK de .NET ofrece una opción más extrema llamada AOT Nativo. En lugar de combinar código IL y nativo, las aplicaciones creadas con AOT nativo sólo contienen código nativo.1 Las funciones de tiempo de ejecución, incluido el recolector de basura y cualquier componente de la biblioteca de tiempo de ejecución que necesite la aplicación, se incluyen en la salida, lo que hace que las aplicaciones AOT nativas sean completamente autónomas. (Las funciones de tiempo de ejecución incluidas no incluyen el compilador JIT. Es innecesario, porque todo el IL se compila en lenguaje máquina en tiempo de compilación). A diferencia de otros modelos de compilación .NET, las aplicaciones AOT nativas no necesitan una copia del tiempo de ejecución .NET preinstalada o enviada junto con el código de la aplicación. No todas las aplicaciones pueden utilizar AOT Nativo, porque algunas bibliotecas .NET explotan la capacidad del CLR para compilar código JIT generando código nuevo en tiempo de ejecución, por lo que no funcionan (o tienen una funcionalidad limitada) en AOT Nativo. Pero en los casos en que es aplicable, puede reducir drásticamente los tiempos de inicio de las aplicaciones que pueden utilizarlo.

El código gestionado tiene información de tipo omnipresente. El tiempo de ejecución de .NET requiere que esté presente para habilitar determinadas funciones. Por ejemplo, .NET ofrece varios servicios de serialización automática, en los que los objetos pueden convertirse en representaciones binarias o textuales de su estado, y esas representaciones pueden volver a convertirse posteriormente en objetos, quizá en una máquina diferente. Este tipo de servicio se basa en una descripción completa y precisa de la estructura de un objeto, algo que está garantizado que esté disponible en el código gestionado. La información sobre tipos puede utilizarse de otras formas. Por ejemplo, los marcos de pruebas unitarias pueden utilizarla para inspeccionar el código de un proyecto de pruebas y descubrir todas las pruebas unitarias que has escrito. Este tipo de funciones suelen depender de los servicios de reflexión del CLR, que son el tema del Capítulo 13. Sin embargo, la AOT nativa impone algunas restricciones: la información de tipos completa está disponible en el momento en que la AOT nativa empieza a generar código nativo, pero a menos que pueda deducir que tu código dependerá de esa información de tipos en tiempo de ejecución, a menudo recortará parte de ella. Esto hace que la salida compilada sea significativamente más pequeña, lo que puede mejorar los tiempos de inicio, pero también significa que, por defecto, la salida final puede tener una imagen incompleta, que es otra razón por la que no todas las bibliotecas funcionan con AOT Nativo. Sin embargo, el equipo .NET pretende que la AOT Nativa sea viable para el mayor número posible de aplicaciones, por lo que las últimas versiones del SDK .NET han añadido funciones de generación de código en tiempo de compilación que pueden reducir la dependencia de la reflexión en tiempo de ejecución. Por ejemplo, puede generar código que permita a las bibliotecas JSON descritas en el Capítulo 15 realizar la serialización sin utilizar la reflexión. Esto sigue dependiendo de que todo el código .NET disponga de la información de tipo completa durante el proceso de compilación; sólo permite eliminarla de la salida final de la compilación.

Aunque la estrecha conexión de C# con el tiempo de ejecución es una de sus principales características definitorias, no es la única. Hay una cierta filosofía que sustenta el diseño de C#.

C# Prefiere la Generalidad a la Especialización

C# favorece las características de propósito general del lenguaje frente a las especializadas. C# va ya por su 12.ª versión principal, y con cada lanzamiento, los diseñadores del lenguaje han tenido en mente escenarios específicos a la hora de diseñar nuevas funciones. Sin embargo, siempre se han esforzado por garantizar que cada elemento que añaden sea útil más allá de estos escenarios primarios.

Por ejemplo, hace varios años, los diseñadores del lenguaje C# decidieron añadir características a C# para que el acceso a bases de datos se sintiera bien integrado en el lenguaje. La tecnología resultante, Language Integrated Query (LINQ, descrita en el Capítulo 10), ciertamente apoya ese objetivo, pero lo consiguieron sin añadir al lenguaje ningún soporte directo para el acceso a datos. En su lugar, el equipo de diseño introdujo una serie de capacidades de apariencia bastante diversa. Entre ellas, un mejor soporte de los modismos de programación funcional, la posibilidad de añadir nuevos métodos a los tipos existentes sin recurrir a la herencia, el soporte de tipos anónimos, la posibilidad de obtener un modelo de objetos que represente la estructura de una expresión y la introducción de una sintaxis de consulta. La última de ellas tiene una conexión obvia con el acceso a los datos, pero el resto son más difíciles de relacionar con la tarea que nos ocupa. No obstante, pueden utilizarse colectivamente de forma que simplifiquen significativamente determinadas tareas de acceso a datos. Pero todas las funciones son útiles por sí mismas, por lo que, además de facilitar el acceso a los datos, permiten una gama mucho más amplia de situaciones. Por ejemplo, estas incorporaciones facilitan mucho el procesamiento de listas, conjuntos y otros grupos de objetos, porque las nuevas funciones funcionan para colecciones de cosas de cualquier origen, no sólo bases de datos.

Una ilustración de esta filosofía de generalidad fue una característica del lenguaje que se prototipó para C#, pero que sus diseñadores decidieron finalmente no llevar adelante. La función te habría permitido escribir XML directamente en tu código fuente, incrustando expresiones para calcular los valores de determinadas partes del contenido en tiempo de ejecución. El prototipo compilaba esto en código que generaba el XML completo en tiempo de ejecución. Microsoft Research lo demostró públicamente, pero al final esta función no llegó a C#, aunque más tarde se incluyó en otro lenguaje .NET, Visual Basic, que también obtuvo algunas funciones de consulta especializadas para extraer información de documentos XML. Las expresiones XML incrustadas son una función relativamente limitada, sólo útil cuando creas documentos XML. En cuanto a la consulta de documentos XML, C# admite esta funcionalidad a través de sus funciones LINQ de uso general, sin necesidad de ninguna función de lenguaje específica para XML. La estrella de XML ha menguado desde que se planteó este concepto de lenguaje, habiendo sido usurpado en muchos casos por JSON (que bien podría ser eclipsado por otra cosa en los próximos años). Si el XML incrustado hubiera llegado a C#, ahora parecería una curiosidad anacrónica.

Las nuevas funciones añadidas en versiones posteriores de C# siguen en la misma línea. Por ejemplo, la relativamente nueva sintaxis de rango (descrita en el Capítulo 5) fue motivada en parte por algunos escenarios de aprendizaje automático e IA, pero la característica no se limita a ningún área de aplicación concreta. Del mismo modo, la matemática genérica es una de las nuevas capacidades más significativas de C# 11.0, pero está habilitada por algunas mejoras de propósito general del sistema de tipos.

Normas e implementaciones de C#

Antes de ponernos manos a la obra con código real, necesitamos saber a qué implementación de C# y a qué tiempo de ejecución nos dirigimos. El organismo de normalización Ecma ha redactado especificaciones que definen el comportamiento del lenguaje y del tiempo de ejecución (ECMA-334 y ECMA-335, respectivamente) para las implementaciones de C#. Esto ha hecho posible que surjan múltiples implementaciones de C# y del tiempo de ejecución. En el momento de escribir estas líneas, hay cuatro de uso generalizado: .NET, Mono, .NET Native (un precursor de .NET Native AOT que aún utilizan las aplicaciones dirigidas a la Plataforma Universal de Windows) y .NET Framework. De forma un tanto confusa, Microsoft está detrás de todas ellas, aunque no empezó así.

Muchos .NET

El proyecto Mono se lanzó en 2001 y no se originó en Microsoft (por eso no lleva .NET en su nombre: puede utilizar el nombre C# porque así es como los estándares llaman al lenguaje, pero en la época anterior a la Fundación .NET, la marca .NET era utilizada exclusivamente por Microsoft). Mono comenzó con el objetivo de permitir el desarrollo de aplicaciones de escritorio Linux en C#, pero luego añadió compatibilidad con iOS y Android. Ese movimiento crucial ayudó a Mono a encontrar su nicho, porque ahora se utiliza principalmente para crear aplicaciones multiplataforma para dispositivos móviles en C#. Mono también introdujo la compatibilidad con WebAssembly (también conocido como WASM) e incluye una implementación del CLR que puede ejecutarse en cualquier navegador web compatible con los estándares, permitiendo que el código C# se ejecute en el lado del cliente en aplicaciones web. A menudo se utiliza junto con un marco de aplicaciones .NET llamado Blazor, que te permite construir interfaces de usuario basadas en HTML mientras utilizas C# para implementar el comportamiento. La combinación Blazor-WASM también convierte a C# en un lenguaje viable para trabajar con plataformas como Electron, que utilizan tecnologías de cliente web para crear aplicaciones de escritorio multiplataforma. (Blazor no requiere WASM: también puede funcionar con código C# compilado normalmente y ejecutado en el tiempo de ejecución .NET; la interfaz de usuario de aplicaciones multiplataforma [MAUI] de .NET aprovecha esto para hacer posible escribir una única aplicación que pueda ejecutarse en Android, iOS, macOS y Windows).

Mono fue código abierto desde el principio y ha contado con el apoyo de diversas empresas a lo largo de su existencia. En 2016, Microsoft adquirió la empresa que gestionaba Mono: Xamarin. Por ahora, Microsoft mantiene Xamarin como una marca distinta, posicionándola como la forma de escribir aplicaciones C# multiplataforma que pueden ejecutarse en dispositivos móviles. La tecnología central de Mono se ha fusionado con la base de código de tiempo de ejecución .NET de Microsoft. Este fue el punto final de varios años de convergencia en los que Mono fue compartiendo cada vez más cosas en común con .NET. Al principio, Mono proporcionabasus propias implementaciones de todo: compilador C#, bibliotecas y el CLR. Pero cuando Microsoft publicó una versión de código abierto de su propio compilador, las herramientas de Mono se pasaron a él. Mono solía tener su propia implementación completa de las bibliotecas de tiempo de ejecución de .NET, pero desde que Microsoft publicó la primera versión de código abierto de .NET, Mono depende cada vez más de ella. En la actualidad, Mono ya noes un .NET independiente, sino una de las dos implementaciones de CLR en el repositorio principal de tiempo de ejecución .NET, lo que permite la compatibilidad conentornos de ejecución móviles y WebAssembly.

¿Qué pasa con las otras tres implementaciones, todas las cuales parecen llamarse .NET? Existe .NET Native, un predecesor de Native AOT. Se utiliza en las aplicaciones de la Plataforma Universal de Windows (UWP), pero se desaconseja a los desarrolladores crear nuevas aplicaciones de esta forma, ya que no hay planes para que ni la UWP ni la antigua .NET Native se actualicen, salvo para corregir errores o cuestiones de seguridad. Así que, en la práctica, sólo tenemos dos versiones actuales, no condenadas al fracaso: .NET Framework (sólo para Windows, de código cerrado) y .NET (multiplataforma, de código abierto). Sin embargo, como ya se ha dicho, Microsoft no tiene previsto añadir ninguna función nueva a .NET Framework (sólo para Windows, de código cerrado), por lo que .NET 8.0 es, de hecho, la única versión actual.

No obstante, .NET Framework sigue utilizándose porque hay un puñado de cosas que puede hacer que .NET 8.0 no puede. .NET Framework sólo funciona en Windows, mientras que .NET 8.0 es compatible con Windows, macOS, iOS y Linux, y aunque esto hace que .NET Framework sea menos ampliamente utilizable, significa que puede soportar algunas características específicas de Windows. Por ejemplo, hay una sección de la Biblioteca de Clases de .NET Framework dedicada a trabajar con los Servicios de Componentes COM+, una función de Windows para alojar componentes que se integran con Microsoft Transaction Server. Esto no es posible en las versiones más recientes y multiplataforma de .NET, porque el código podría estar ejecutándose en Linux, donde no existen funciones equivalentes o son demasiado diferentes para presentarlas a través de la misma API .NET.

El número de características exclusivas de .NET se ha reducido drásticamente en las últimas versiones, porque Microsoft ha estado trabajando para que incluso las aplicaciones exclusivas de Windows puedan utilizar la última versión de .NET. Incluso muchas funciones específicas de Windows pueden utilizarse desde .NET. Por ejemplo, la biblioteca System.Speech.NET sólo estaba disponible en .NET Framework porque proporciona acceso a la funcionalidad de reconocimiento y síntesis de voz específica de Windows, pero ahora existe una versión .NET de esta biblioteca. Esa biblioteca sólo funciona en Windows, pero su disponibilidad significa que los desarrolladores de aplicaciones que confían en ella son ahora libres de pasar de .NET Framework a .NET. El resto de funciones de .NET Framework que no se han sacado adelante son las que no se utilizan lo suficiente como para justificar el esfuerzo de ingeniería. La compatibilidad con COM+ no era sólo una biblioteca: tenía implicaciones en la forma en que el CLR ejecutaba el código, por lo que su compatibilidad con el .NET moderno habría tenido unos costes que no se justificaban para lo que ahora es una característica poco utilizada.

La multiplataforma .NET es donde se ha producido la mayor parte del nuevo desarrollo de .NET en los últimos años. .NET Framework sigue recibiendo soporte, y lo seguirá recibiendo durante muchos años, pero Microsoft ha declarado que no obtendrá nuevas funciones, y lleva un tiempo quedándose atrás. Por ejemplo, el marco de aplicaciones web de Microsoft, ASP.NET Core, dejó de dar soporte a .NET Framework en 2019. Así que la retirada de .NET Framework, y el estatus de .NET como la única y verdadera .NET, es la conclusión inevitable de un proceso que lleva en marcha unos cuantos años. Dado que muchos proyectos heredados siguen ejecutándose en .NET Framework, los desarrolladores de bibliotecas .NET a menudo desean dar soporte a ambos tiempos de ejecución, por lo que señalaré los lugares donde existen diferencias significativas, pero las nuevas aplicaciones C# deben utilizar .NET, no .NET Framework.

Ciclos de publicación y asistencia a largo plazo

Actualmente, Microsoft publica nuevas versiones de C# y .NET cada año, normalmente hacia noviembre o diciembre, pero no todas las versiones son iguales. Las versiones alternativas obtienen Soporte a Largo Plazo (LTS), lo que significa que Microsoft se compromete a dar soporte a la versión durante al menos tres años. Durante ese periodo, las herramientas, las bibliotecas y el tiempo de ejecución se actualizarán periódicamente con parches de seguridad. .NET 8.0, publicada en noviembre de 2023, es una versión LTS, por lo que recibirá soporte hasta diciembre de 2026. La versión LTS precedente fue .NET 6.0, que se publicó en diciembre de 2021 y, por tanto, sigue en soporte hasta diciembre de 2024; la versión LTS anterior fue .NET Core 3.1,2 que dejó de recibir soporte en diciembre de 2022.

¿Qué pasa con las versiones que no son LTS? Éstas reciben soporte desde su lanzamiento, pero sólo durante 18 meses. Por ejemplo, .NET 5.0 recibió soporte cuando se lanzó en diciembre de 2020, pero el soporte finalizó en mayo de 2022, seis meses después de que se lanzara .NET 6.0 (y seis meses antes de que finalizara el soporte para su predecesora .NET Core 3.1).

A menudo el ecosistema tarda unos meses en ponerse al día con una nueva versión. En la práctica, es posible que no puedas utilizar una nueva versión de .NET el día de su lanzamiento, porque tu proveedor de la plataforma en la nube puede que aún no la admita, o puede haber incompatibilidades con bibliotecas que necesites utilizar. Esto acorta significativamente la vida útil efectiva de las versiones que no son LTS, y puede dejarte con una ventana incómodamente estrecha en la que actualizarte cuando aparezca la siguiente versión. Si las herramientas, plataformas y bibliotecas de las que dependes tardan unos meses en adaptarse a la nueva versión, tendrás muy poco tiempo para seguir adelante antes de que deje de recibir soporte. En situaciones extremas, puede que ni siquiera exista esta ventana de oportunidad: .NET Core 2.2 llegó al final de su vida útil soportada antes de que Azure Functions ofreciera soporte completo para .NET Core 3.0 o 3.1, por lo que los desarrolladores que habían utilizado .NET Core 2.2 no LTS en Azure Functions se encontraron en una situación en la que la última versión soportada en realidad retrocedía: tenían que elegir entre volver a .NET Core 2.1 o utilizar un tiempo de ejecución no soportado en producción durante unos meses. Por este motivo, algunos desarrolladores consideran las versiones que no son LTS como versiones preliminares: puedes probar nuevas funciones antes de utilizarlas en producción cuando lleguen a una versión LTS.

Dirigido a varios tiempos de ejecución .NET

Durante muchos años, la multiplicidad de tiempos de ejecución de .NET, cada uno con su propia versión diferente de las bibliotecas de tiempo de ejecución, supuso un reto para cualquiera que quisiera poner su código C# a disposición de otros desarrolladores. Esto ha mejorado en los últimos años porque Microsoft ha hecho de la convergencia uno de sus principales objetivos en las últimas versiones, de modo que hoy en día, si un componente está orientado a la versión más antigua de .NET compatible (.NET 6.0 en el momento de escribir esto), podrá ejecutarse en la mayoría de los tiempos de ejecución .NET. Sin embargo, es habitual querer seguir dando soporte a sistemas que funcionan con el antiguo .NET Framework. Esto significa que será útil producir componentes que se dirijan a múltiples tiempos de ejecución .NET enun futuroprevisible.

Existe un repositorio de paquetes para componentes .NET llamado NuGet, que es donde Microsoft publica todas las bibliotecas .NET que produce y que no están integradas en la propia .NET, y también es donde la mayoría de los desarrolladores .NET publican las bibliotecas que les gustaría compartir. Pero, ¿para qué versión deberías construir? Se trata de una cuestión bidimensional: está la implementación en tiempo de ejecución (.NET, .NET Framework, UWP) y también la versión (por ejemplo, .NET 6.0 o .NET 8.0; .NET Framework 4.7.2 o 4.8). Muchos autores de paquetes populares de código abierto distribuidos a través de NuGet admiten una plétora de versiones, antiguas y nuevas.

Los autores de componentes solían admitir varios tiempos de ejecución creando múltiples variantes de sus bibliotecas. Cuando distribuyes bibliotecas .NET a través de NuGet, puedes incrustar varios conjuntos de binarios en el paquete, cada uno dirigido a diferentes sabores de .NET. Sin embargo, uno de los principales problemas de esto es que, a medida que han ido apareciendo nuevas formas de .NET a lo largo de los años, las bibliotecas existentes no se ejecutarían en todos los tiempos de ejecución más recientes. Un componente escrito para .NET Framework 4.0 funcionaría en todas las versiones posteriores de .NET Framework, pero no necesariamente en, digamos, .NET 6.0. Aunque el código fuente del componente fuera totalmente compatible con el nuevo tiempo de ejecución, tendrías que compilar una versión distinta para esa plataforma. Y si el autor de una biblioteca que utilizas sólo hubiera proporcionado binarios de .NET Framework, podría no funcionar en .NET. (Puedes decirle a .NET que intente utilizar binarios .NET Framework, y hará todo lo posible para adaptarse a ellos. Puede que descubras que simplemente funciona, pero no hay garantías). Esto era malo para todos. Varias versiones de .NET han ido y venido a lo largo de los años (como Silverlight y varias variantes de Windows Phone; .NET Native de UWP sigue ahí), lo que significa que los autores de componentes se encontraron en la rueda de molino de tener que producir nuevas variantes de su componente. Como eso depende de que esos autores tengan la disposición y el tiempo para hacer ese trabajo, los consumidores de componentes pueden encontrarse con que no todos los componentes que quieren utilizar están disponibles en la plataforma que han elegido.

Para evitarlo, Microsoft introdujo .NET Standard, que define subconjuntos comunes de la superficie API de las bibliotecas de tiempo de ejecución .NET. Muchos de los tiempos de ejecución más antiguos han desaparecido, pero la división entre .NET y .NET Framework se mantiene, por lo que hoy en día, .NET Standard 2.0 es probablemente la mejor opción para los autores de componentes que deseen admitir una amplia gama de plataformas, porque todas las versiones de .NET publicadas recientemente lo admiten, y proporciona acceso a un conjunto muy amplio de funciones. Si no necesitas admitir .NET Framework, tendría más sentido orientarte a .NET 6.0 o .NET 8.0 en su lugar. En el Capítulo 12 se describen con más detalle algunas de las consideraciones en torno a .NET Standard.

Nota

Cuando una versión de .NET deja de ser compatible, los componentes para ella no quedan obsoletos. .NET admite el uso de componentes creados para versiones anteriores, de modo que si encuentras un componente en NuGet orientado a .NET 5.0, puedes utilizarlo en .NET 8.0. Sería conveniente comprobar si dicho componente sigue en desarrollo activo, pero los objetivos antiguos no implican necesariamente que un componente esté obsoleto. A veces, los autores de componentes deciden ayudar a las personas que ejecutan sistemas en tiempos de ejecución no compatibles.

Microsoft proporciona algo más que un lenguaje y los distintos tiempos de ejecución con sus bibliotecas de clases asociadas. También hay entornos de desarrollo que pueden ayudarte a escribir, probar, depurar y mantener tu código.

Visual Studio, Visual Studio Code y JetBrains Rider

Microsoft ofrece dos entornos de desarrollo de escritorio: Visual Studio Code y Visual Studio. Ambos proporcionan las funciones básicas -como un editor de texto, herramientas de compilación y un depurador-, pero Visual Studio ofrece el soporte más amplio para desarrollar aplicaciones C#, tanto si esas aplicaciones se ejecutarán en Windows como en otras plataformas. Es el que lleva más tiempo en el mercado -tanto como C#-, por lo que procede de la época anterior al código abierto y sigue siendo un producto de código cerrado. Las distintas ediciones disponibles van desde las gratuitas hasta las más caras. Microsoft no es la única opción: la empresa de productividad para desarrolladores JetBrains vende un IDE .NET completo llamado Rider, que funciona en Windows, Linux y macOS.

Visual Studio es un Entorno de Desarrollo Integrado (IDE), por lo que adopta un enfoque de "todo incluido". Además de un editor de texto con todas las funciones, ofrece herramientas de edición visual para interfaces de usuario. Existe una profunda integración con sistemas de control de código fuente como Git y con sistemas en línea como GitHub y el sistema Azure DevOps de Microsoft, que proporcionan repositorios de código fuente, seguimiento de incidencias y otras funciones de gestión del ciclo de vida de las aplicaciones (ALM). Visual Studio ofrece herramientas integradas de monitoreo y diagnóstico del rendimiento. Dispone de varias funciones para trabajar con aplicaciones desarrolladas e implementadas en la plataforma en nube Azure de Microsoft. Tiene el conjunto más amplio de funciones de refactorización de los tres entornos de Microsoft descritos aquí. Ten en cuenta que esta versión de Visual Studio sólo funciona en Windows.

El IDE JetBrains Rider es un único producto que funciona en Windows, macOS y Linux. Está más centrado que Visual Studio, en el sentido de que se diseñó exclusivamente para dar soporte al desarrollo de aplicaciones .NET (Visual Studio también da soporte a C++). (Visual Studio también es compatible con C++.) Tiene un enfoque similar de "todo incluido", y ofrece una gama especialmente potente de herramientas de refactorización.

Visual Studio Code (a menudo abreviado VS Code) se publicó por primera vez en 2015. Es de código abierto y multiplataforma, compatible con Linux, Windows y Mac. Se basa en la plataforma Electron y está escrito principalmente en TypeScript. (Esto significa que VS Code es realmente el mismo programa en todos los sistemas operativos). VS Code es un producto más ligero que Visual Studio: una instalación básica de VS Code tiene poco más que soporte de edición de texto. Sin embargo, a medida que abras archivos, descubrirá extensiones descargables que, si decides instalarlas, pueden añadir soporte para C#, F#, TypeScript, PowerShell, Python y una amplia gama de otros lenguajes. (El mecanismo de extensiones es abierto, así que cualquiera que lo desee puede publicar una extensión). Así que, aunque en su forma inicial es menos un IDE y más un simple editor de texto, su modelo de extensibilidad lo hace bastante potente. La amplia gama de extensiones ha hecho que VS Code se haya hecho notablemente popular fuera del mundo de los lenguajes de Microsoft, y esto a su vez ha fomentado un círculo virtuoso de crecimiento aún mayor de la gama deextensiones.

Visual Studio y JetBrains Rider ofrecen el camino más directo para iniciarse en C#: no necesitas instalar ninguna extensión ni modificar ninguna configuración para ponerte a trabajar. Sin embargo, Visual Studio Code está disponible para un público más amplio, así que lo utilizaré en la rápida introducción al trabajo con C# que sigue. No obstante, los mismos conceptos básicos se aplican a todos los entornos, así que si vas a utilizar Visual Studio o Rider, la mayor parte de lo que describo aquí sigue siendo válido.

Consejo

Puedes descargarte Visual Studio Code gratuitamente. También necesitarás instalar el SDK .NET. Si utilizas Windows y prefieres utilizar Visual Studio, puedes descargar la versión gratuita de Visual Studio, llamada Visual Studio Community. Esto instalará el SDK .NET por ti, siempre que selecciones al menos una carga de trabajo.NET durante la instalación.

Cualquier aplicación C# no trivial tendrá varios archivos de código fuente, y éstos pertenecerán a un proyecto. Cada proyecto genera un único resultado, u objetivo. El objetivo de compilación puede ser tan simple como un único archivo -un proyecto C# puede producir un archivo ejecutable o una biblioteca, por ejemplo-, pero algunos proyectos producen resultados más complicados. Por ejemplo, algunos tipos de proyectos crean sitios web. Un sitio web normalmente contendrá varios archivos, pero colectivamente, estos archivos representan una única entidad: un sitio web. El resultado de cada proyecto se implementará como una unidad, aunque conste de varios archivos.

Los ejecutables suelen tener una extensión de archivo .exe en Windows, mientras que las bibliotecas utilizan .dll (históricamente abreviatura de biblioteca de vínculos dinámicos). Con .NET, sin embargo, todo el código va en archivos .dll, incluso en macOS y Linux. El SDK también puede generar un ejecutable anfitrión (con extensión .exe en Windows), pero éste sólo inicia el tiempo de ejecución y luego carga la .dll que contiene la salida principal compilada. (Es ligeramente diferente si te diriges a .NET Framework: eso compila la aplicación directamente en un .exe autoarrancable sin .dll independiente). En cualquier caso, la única diferencia entre la salida compilada principal de una aplicación y una biblioteca es que la primera especifica un punto de entrada de la aplicación. Ambos tipos de archivos pueden exportar funciones para que las consuman otros componentes. Ambos son ejemplos de ensamblajes, tema del Capítulo 12. (Si utilizas AOT Nativo acabarás con un .exe en Windows y un binario ejecutable similar en otras plataformas, pero AOT Nativo funciona esencialmente como un paso final extra: toma los diversos archivos .dll producidos por el proceso de compilación normal de .NET y los compila en un único ejecutable nativo).

Los archivos de proyecto de C# tienen una extensión .csproj, y si los examinas con un editor de texto, verás que contienen XML. Un archivo .csproj describe el contenido del proyecto y configura cómo debe construirse. Los IDEs Visual Studio y Rider saben cómo procesarlos, y también las extensiones .NET para VS Code. También los entienden varias utilidades de construcción de línea de comandos, como la herramienta de línea de comandos dotnet instalada por el SDK .NET, y también la antigua herramienta MSBuild de Microsoft. (MSBuild admite numerosos lenguajes y objetivos, no sólo .NET. De hecho, cuando construyes un proyecto C# con el comando dotnet build del SDK .NET, se trata en realidad de una envoltura alrededor de MSBuild).

A menudo querrás trabajar con grupos de proyectos. Por ejemplo, es una buena práctica escribir pruebas para tu código, pero la mayor parte del código de prueba no necesita desplegarse como parte de la aplicación, por lo que normalmente pondrías las pruebas automatizadas en proyectos separados. Y puede que quieras dividir tu código por otras razones. Quizás el sistema que estás construyendo tiene una aplicación de escritorio y un sitio web, y tienes código común que te gustaría utilizar en ambas aplicaciones. En este caso, necesitarías un proyecto que construya una biblioteca que contenga el código común, otro que produzca el ejecutable de la aplicación de escritorio, otro para construir el sitio web, y tres proyectos más que contengan las pruebas para cada uno de los proyectos principales.

Las herramientas de compilación y los IDE que entienden .NET te ayudan a trabajar con múltiples proyectos relacionados a través de lo que denominan una solución. Una solución es un archivo con extensión .sln, que define una colección de proyectos. Aunque los proyectos de una solución suelen estar relacionados, no tienen por qué estarlo.

Si utilizas Visual Studio, ten en cuenta que exige que los proyectos pertenezcan a una solución, aunque sólo tengas un proyecto. Visual Studio Code está encantado de abrir un solo proyecto si quieres, pero sus extensiones .NET también reconocen las soluciones.

Un proyecto puede pertenecer a más de una solución. En una gran base de código, es habitual tener varios archivos .sln con diferentes combinaciones de proyectos. Lo normal es que tengas una solución principal que contenga todos y cada uno de los proyectos, pero no todos los desarrolladores querrán trabajar con todo el código todo el tiempo. Alguien que trabaje en la aplicación de escritorio de nuestro ejemplo hipotético también querrá la biblioteca compartida, pero probablemente no tenga interés en cargar el proyecto web.

Mostraré cómo crear un nuevo proyecto, abrirlo en Visual Studio Code y ejecutarlo. A continuación, recorreré las distintas características de un nuevo proyecto C# como introducción al lenguaje. También mostraré cómo añadir un proyecto de pruebas unitarias y cómo crear una solución que contenga ambos proyectos.

Anatomía de un programa sencillo

Una vez que hayas instalado el SDK de .NET 8.0 directamente o instalando un IDE, puedes crear un nuevo programa .NET. Empieza por crear un nuevo directorio llamado HolaMundo en tu ordenador para guardar el código. Abre un símbolo del sistema y asegúrate de que su directorio actual es ese, y luego ejecuta este comando:

dotnet new console

Esto crea una nueva aplicación de consola C# creando dos archivos. Crea un archivo de proyecto con un nombre basado en el directorio padre: HolaMundo.csproj en este caso. Y habrá un archivo Program.cs que contendrá el código. Si abres ese archivo en un editor de texto, verás que es bastante sencillo, como muestra el Ejemplo 1-1.

Ejemplo 1-1. Nuestro primer programa
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");

Puedes compilar y ejecutar este programa con el siguiente comando:

dotnet run

Como probablemente ya habrás adivinado, esto mostrará el texto Hello, World!, el comportamiento tradicional para el ejemplo de apertura en cualquier libro de programación.

Más de la mitad de este ejemplo es sólo un comentario. La segunda línea aquí es todo lo que necesitas, y la primera sólo muestra un enlace que explica el "nuevo" estilo que utiliza este proyecto. Ya no es tan nuevo -se incorporó con el SDK de .NET 6.0-, pero se ha producido un cambio significativo en el lenguaje, y los autores del SDK de .NET consideraron necesario dar una explicación. Las últimas versiones de C# han añadido varias funciones destinadas a reducir la cantidad de "boilerplate". Boilerplate es el nombre utilizado para describir el código que debe estar presente para satisfacer ciertas reglas o convenciones, pero que tiene más o menos el mismo aspecto en cualquier proyecto. Por ejemplo, C# exige que el código se defina dentro de un método, y un método debe definirse siempre dentro de un tipo. Puedes ver pruebas de estas reglas en el Ejemplo 1-1. Para producir la salida, se basa en la capacidad del tiempo de ejecución .NET para mostrar texto, que se materializa en un método llamado WriteLine. Pero no decimos simplemente WriteLine porque los métodos C# siempre pertenecen a tipos, por lo que el código lo califica como Console.WriteLine.

Cualquier código C# que escribamos está sujeto a las reglas, así que nuestro código que invoque al método Console.WriteLine debe vivir a su vez dentro de un método dentro de un tipo. Y en la mayoría del código C#, esto sería explícito: en la mayoría de los casos, verás algo más parecido al Ejemplo 1-2.

Ejemplo 1-2. "¡Hola, mundo!" con texto repetitivo visible
using System;

namespace HelloWorld;

internal class Program
{
    private static void Main(string[] args)
    {
        Console.WriteLine("Hello, World!");
    }
}

Aquí sigue habiendo una sola línea que define el comportamiento de la aplicación, y es la misma que en el Ejemplo 1-1. La ventaja obvia del primer ejemplo es que nos permite centrarnos en lo que realmente hace nuestro programa, aunque el inconveniente es que gran parte de lo que ocurre se vuelve invisible. Con el estilo explícito del Ejemplo 1-2, nada queda oculto. El Ejemplo 1-1 utiliza el estilo de declaración de nivel superior, pero el compilador sigue poniendo el código en un método definido dentro de un tipo llamado Program; sólo que no puedes verlo en el código. En el Ejemplo 1-2, el método y el tipo son claramente visibles.

La función de reducción de boilerplate de C# que nos permite sumergirnos directamente en el código es sólo para el punto de entrada del programa. Cuando escribes el código que quieres que se ejecute cada vez que se inicie tu programa, no necesitas definir una clase o método que lo contenga. Pero un programa sólo tiene un punto de entrada, y para todo lo demás, sigues necesitando definirlo. Así que, en la práctica, la mayor parte del código C# se parece más al Ejemplo 1-2 que al Ejemplo 1-1, y en las bases de código más antiguas incluso el punto de entrada del programa utilizará este estilo explícito.

Como los proyectos reales implican varios archivos, y normalmente varios proyectos, pasemos a un ejemplo un poco más realista. Voy a crear un programa que calcule la media (la media aritmética, para ser precisos) de algunos números. También crearé un segundo proyecto que comprobará automáticamente el primero. Como tengo dos proyectos, esta vez necesitaré una solución. Crearé un nuevo directorio llamado Promedios. Si me estás siguiendo, no importa dónde vaya, aunque es buena idea no ponerlo dentro del directorio del proyecto HolaMundo. Abriré un símbolo del sistema en el directorio Promedios y ejecutaré este comando:

dotnet new sln

Esto creará un nuevo archivo de solución llamado Promedios.sln. (Por defecto, dotnet newsuele asignar a los nuevos proyectos y soluciones los nombres de los directorios que los contienen, aunque puedes especificar otros nombres). Ahora añadiré los dos proyectos que necesito con estosdos comandos:

dotnet new console -o Averages
dotnet new mstest -o Averages.Tests

La opción -o (abreviatura de output) indica que quiero que la herramienta cree un nuevo subdirectorio para cada uno de estos nuevos proyectos: cuando tienes varios proyectos, cada uno necesita su propio directorio.

Ahora tengo que añadirlas a la solución:

dotnet sln add ./Averages/Averages.csproj
dotnet sln add ./Averages.Tests/Averages.Tests.csproj

Voy a utilizar ese segundo proyecto para definir algunas pruebas que comprobarán el código del primer proyecto (por eso he especificado un tipo de proyecto mstest-este proyecto utilizará el marco de pruebas unitarias de Microsoft). Esto significa que el segundo proyecto necesitará acceder al código del primer proyecto. Para permitirlo, ejecuto este comando:

dotnet add ./Averages.Tests/Averages.Tests.csproj reference
./Averages/Averages.csproj

(He dividido esto en dos líneas para que quepa, pero debe ejecutarse como un único comando). Por último, para editar el proyecto, puedo lanzar VS Code en el directorio actual con este comando:

code .

Si le estás siguiendo la corriente, y si es la primera vez que ejecutas VS Code, te pedirá que tomes algunas decisiones, como elegir un esquema de colores. También podría preguntarte si confías en la ubicación de la carpeta, en cuyo caso deberías decirle que sí. Puedes tener la tentación de ignorar sus preguntas, pero una de las cosas que puede ofrecerte hacer en este momento es instalar extensiones para el soporte de idiomas. La gente utiliza VS Code con todo tipo de lenguajes, y el instalador no hace suposiciones sobre cuál vas a utilizar, así que tienes que instalar una extensión para obtener soporte de C#. Si sigues las instrucciones de VS Code para buscar extensiones de idiomas, te ofrecerá la extensión C# Dev Kit de Microsoft. No te asustes si VS Code no te lo ofrece. El comportamiento de la sugerencia automática de extensiones ha cambiado de vez en cuando, y puede que haya vuelto a cambiar después de que yo escribiera esto. Puede que tengas que abrir uno de los archivos .cs antes de que te muestre una sugerenciasimilar a la de la Figura 1-1.

Visual Studio Code showing a popup offering to install the C# Dev Kit
Figura 1-1. Sugerencia de extensión de Visual Studio Code

Si no lo ves, es posible que ya tuvieras instalada la extensión C# Dev Kit. Para averiguarlo, haz clic en el icono Extensiones de la barra de la izquierda. Es el que se muestra en la parte inferior izquierda de la Figura 1-2, con cuatro cuadrados. Si has abierto código VS en un directorio que contiene un archivo .csproj, la extensión C# Dev Kit debería aparecer en la sección INSTALADO si ya la tienes instalada, y en lasección RECOMENDADO si aún no la tienes.

Visual Studio Code's C# Dev Kit Extension
Figura 1-2. Extensión C# Dev Kit de Visual Studio Code

Si sigues sin verlo, escribe C# en el cuadro de texto de búsqueda de la parte superior. Aparecerán unos cuantos resultados, así que, si estás siguiendo el proceso, asegúrate de que eliges el correcto. Si haces clic en el resultado de la búsqueda, aparecerá información más detallada, que debería mostrar su nombre completo como "C# Dev Kit" con un subtítulo de "Extensión oficial de C# de Microsoft" y mostrará a "Microsoft" como editor. Haz clic en el botón Instalar para instalar la extensión.

Puede que tarde unos minutos en descargar e instalar la extensión C# Dev Kit, pero una vez hecho esto, en la parte inferior izquierda de la ventana la barra de estado debería tener un aspecto similar al de la Figura 1-3, mostrando que ha encontrado los dos proyectos que hemos creado.

Visual Studio Code's status bar showing that it has found two projects
Figura 1-3. Barra de estado de Visual Studio Code

La extensión C# Dev Kit inspeccionará todo el código fuente de todos los proyectos de la solución. Obviamente, aún no hay mucho en ellos, pero seguirá analizando el código mientras escribo, lo que le permitirá identificar problemas y hacer sugerencias útiles.

Puedo echar un vistazo al código cambiando a la vista Explorador, utilizando el botón situado en la parte superior de la barra de herramientas de la izquierda. Como muestra la Figura 1-4, muestra los directorios y archivos. He expandido el directorio Promedios.Pruebas y he seleccionado su archivoUnitTest1.cs.

Visual Studio Code's Explorer, with the Averages.Test project expanded, and the UnitTest1.cs file selected
Figura 1-4. Explorador de Visual Studio Code
Consejo

Si haces un solo clic en un archivo del panel del Explorador, VS Code lo muestra en una pestaña de vista previa, lo que significa que no permanecerá abierto durante mucho tiempo: en cuanto hagas clic en algún otro archivo, éste desplazará al que tenías abierto antes. Esto está pensado para evitar acabar con cientos de pestañas abiertas, pero si estás trabajando una y otra vez en dos archivos, puede resultar molesto. Puedes evitarlo haciendo doble clic en el archivo al abrirlo: se abrirá una pestaña que no es de vista previa, que permanecerá abierta hasta que la cierres deliberadamente. Si ya tienes un archivo abierto en una pestaña de vista previa, puedes hacer doble clic en el nombre del archivo para convertirlo en una pestaña normal. VS Code muestra el nombre del archivo en cursiva en las pestañas de vista previa, y verás que cambia a no cursiva cuando hagas doble clic.

Quizá te preguntes por qué he ampliado el directorio Promedios.Pruebas. El propósito de este proyecto de pruebas será garantizar que el proyecto principal hace lo que se supone que debe hacer. Con este ejemplo, seguiré la práctica de ingeniería de definir pruebas que encarnen mis requisitos antes de escribir el código que se va a probar (lo que a veces se conoce como desarrollo dirigido por pruebas o TDD). Eso significa empezar con el proyecto de pruebas.

Escribir una prueba unitaria

Cuando ejecuté antes el comando para crear este proyecto, especifiqué un tipo de proyecto de mstest. Esta plantilla de proyecto me ha proporcionado una clase de prueba para empezar, en un archivo llamado UnitTest1.cs. Quiero elegir un nombre más informativo. Hay varias escuelas de pensamiento sobre cómo debes estructurar tus pruebas unitarias. Algunos desarrolladores abogan por una clase de prueba para cada clase que desees probar, pero a mí me gusta el estilo en el que escribes una clase para cada escenario en el que quieras probar una clase concreta, con un método para cada una de las cosas que deberían ser ciertas sobre tu código en ese escenario. Este programa sólo tendrá un comportamiento: calculará la media aritmética de sus entradas. Así que cambiaré el nombre del archivo fuente UnitTest1.cs a CuandoCalculandoPromedios.cs. (Puedes renombrar un archivo haciendo clic con el botón derecho del ratón en el panel Explorador de VS Code y seleccionando la entrada Renombrar). Esta prueba debería verificar que obtenemos los resultados esperados para unas cuantas entradas representativas. El Ejemplo 1-3 muestra un archivo fuente completo que hace esto; aquí hay dos pruebas, mostradas en negrita.

Ejemplo 1-3. Una clase de prueba unitaria para nuestro primer programa
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Averages.Tests;

[TestClass]
public class WhenCalculatingAverages
{
    [TestMethod]
    public void SingleInputShouldProduceSameValueAsResult()
    {
        string[] inputs = { "1" };
        double result = AverageCalculator.ArithmeticMean(inputs);
        Assert.AreEqual(1.0, result, 1E-14);
    }

    [TestMethod]
    public void MultipleInputsShouldProduceAverageAsResult()
    {
        string[] inputs = { "1", "2", "3" };
        double result = AverageCalculator.ArithmeticMean(inputs);
        Assert.AreEqual(2.0, result, 1E-14);
    }
}

Explicaré cada una de las funciones de este archivo una vez que haya mostrado el programa en sí. Por ahora, las partes más interesantes de este ejemplo son los dos métodos. En primer lugar, tenemos el método SingleInputShouldProduceSameValueAsResult, que comprueba que nuestro programa maneja correctamente el caso en el que hay una única entrada. La primera línea de este método describe la entrada: un único número. (Sorprendentemente, esta pruebarepresenta los números como cadenas. Esto se debe a que nuestras entradas vendrán en última instancia como argumentos de línea de comandos, por lo que nuestra prueba debe reflejarlo). La segunda línea ejecuta el código bajo prueba (que en realidad aún no he escrito). Y la tercera línea establece que la media calculada debe ser igual a la única entrada. Si no lo es, esta prueba informará de un fallo. El segundo método, MultipleInputsShouldProduceAverageAsResult, comprueba un caso algo más complejo, en el que hay tres entradas, pero tiene la misma forma básica que el primero.

Nota

Este código utiliza el tipo double de C#, un número de coma flotante de doble precisión, para poder representar resultados que no sean números enteros. En elpróximo capítulo describiré con más detalle los tipos de datos incorporados de C#, pero ten en cuenta que, como en la mayoría de los lenguajes de programación, la aritmética de coma flotante en C# tiene una precisión limitada. El método Assert.AreEqual que estoy utilizando para comprobar los resultados aquí tiene esto en cuenta y me permite especificar la tolerancia máxima para la imprecisión. El argumento final de 1E-14 en cada caso denota el número 1 dividido por 10 elevado a la potencia de 14, por lo que estas pruebas están indicando que requieren que la respuesta sea correcta con 14 decimales.

Centrémonos en una línea concreta de estas pruebas: la que ejecuta el código que quiero probar. El Ejemplo 1-4 muestra la línea correspondiente del Ejemplo 1-3. Así es como se invoca un método que devuelve un resultado en C#. Esta línea comienza declarando una variable para contener el resultado. (El texto double indica el tipo de datos, y result es el nombre de la variable.) Todos los métodos en C# deben definirse dentro de un tipo, así que, al igual que vimos antes con el ejemplo de Console.WriteLine, aquí tenemos la misma forma: un nombre de tipo, luego un punto, luego un nombre de método. Y luego, entre paréntesis, la entrada al método.

Ejemplo 1-4. Llamar a un método
double result = AverageCalculator.ArithmeticMean(inputs);

Si sigues el código escribiendo a medida que lo lees, si te fijas en los dos lugares en los que aparece esta línea de código (una vez en cada método de prueba), te darás cuenta de que VS Code ha dibujado una línea garabateada debajo de AverageCalculator. Si pasas el ratón por encima de este tipo de garabato, aparecerá un mensaje de error, como muestra la Figura 1-5.

Visual Studio Code showing the AverageCalculator symbol underlined, and an error popup containing this text: The name AverageCalculator does not exist in the current context Averages.Tests
Figura 1-5. Un tipo no reconocido

Esto nos está diciendo algo que ya sabíamos: todavía no he escrito el código que esta prueba pretende probar. Vamos a solucionarlo. Necesito añadir un nuevo archivo, lo que puedo hacer en la vista del Explorador de VS Code haciendo clic en el directorio Promedios y luego, con él seleccionado, haciendo clic en el botón situado más a la izquierda de la barra de herramientas, cerca de la parte superior del Explorador. La Figura 1-6 muestra que cuando pasas el ratón por encima de este botón, aparece una información sobre la herramienta que confirma su finalidad. Tras pulsarlo, puedo escribir CalculadoraPromedio.cs como nombre para el nuevo archivo.

Visual Studio Code's Explorer view, with the New File button highlighter, and a tooltip saying 'New File'
Figura 1-6. Añadir un nuevo archivo

VS Code creará un nuevo archivo vacío. Añadiré la menor cantidad de código que pueda para solucionar el error que aparece en la Figura 1-5. El Ejemplo 1-5 satisfará al compilador de C#. Aún no está completo: no realiza los cálculos necesarios, pero ya llegaremos a eso.

Ejemplo 1-5. Una clase simple
namespace Averages;

public static class AverageCalculator
{
    public static double ArithmeticMean(string[] args)
    {
        return 1.0;
    }
}

Ahora puedes construir el código. Si miras en la parte inferior del Explorador de VS Code, verás una sección Explorador de soluciones. Si la despliegas, aparecerá la solución y sus dos proyectos. Puedes hacer clic con el botón derecho en la solución y seleccionar Compilar, como muestra la Figura 1-7. También puedes escribir dotnet build en la línea de comandos. O puedes utilizar el atajo de teclado Ctrl-Shift-B. La primera vez que utilices ese acceso directo, puede que te pida que confirmes que quieres utilizar el comando dotnet build para realizar la compilación.

The Solution Explorer in Visual Studio Code, showing the menu for the Averages solution, with the Build menu item highlighted
Figura 1-7. Construir la solución

Una vez finalizada la compilación, deberías ver un icono de un matraz en la barra de botones de la izquierda de la ventana de VS Code, como muestra la Figura 1-8. Si haces clic en él, aparecerá el panel Pruebas, que te permitirá ejecutar pruebas y ver los resultados.

Visual Studio Code's Testing button (a flask icon)
Figura 1-8. El botón Pruebas

El panel Pruebas muestra las pruebas jerárquicamente, primero agrupadas por proyecto y luego por espacio de nombres. (Como tu proyecto de pruebas se llama Averages.Tests, y todas sus pruebas están en un espacio de nombres llamado Averages.Tests, verás que aparece dos veces, como muestra la Figura 1-9 ). En la parte superior del panel Pruebas hay una fila de botones. Uno muestra un par de triángulos, uno encima del otro. Si haces clic en él, se ejecutarán todas las pruebas.

Visual Studio Code showing test results, indicating that the 'SingleInputShouldProduceSameValueAsResult' test passed, and the 'MultipleInputsShouldProduceAverageAsResult' test failed
Figura 1-9. Resultados de las pruebas

Como aún no hemos terminado de escribir la biblioteca, una de estas pruebas fallará. Como muestra la Figura 1-9, esto se indica mostrando una cruz junto a la prueba. Si haces clic en la prueba que falla, VS Code te mostrará el punto del código en el que falló la prueba, junto con detalles como el mensaje de fallo y un seguimiento de pila, como muestra la Figura 1-10.

Visual Studio Code showing that the 'MultipleInputsShouldProduceAverageAsResult' test failed in its call to Assert.AreEqual with a message: Expected a difference no greater than 1E-14 between expected value 2 and actual value 1
Figura 1-10. Una prueba fallida

También puedes ejecutar pruebas con el comando dotnet test. Muestra la misma información que Visual Studio Code, pero como salida de consola normal:

  Failed MultipleInputsShouldProduceAverageAsResult [291 ms]
  Error Message:
   Assert.AreEqual failed. Expected a difference no greater than <1E-14>
 between expected value <2> and actual value <1>.
  Stack Trace:
     at Averages.Tests.WhenCalculatingAverages.
MultipleInputsShouldProduceAverageAsResult() in
C:\book\Averages\Averages.Tests\WhenCalculatingAverages.cs:line 21
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments,
 Signature sig, Boolean isConstructor)
   at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj,
 BindingFlags invokeAttr)

Failed!  - Failed:     1, Passed:     1, Skipped:     0, Total:     2,
Duration: 364 ms - Averages.Tests.dll (net6.0)

Como era de esperar, obtenemos fallos porque aún no he escrito una implementación adecuada. Pero antes, quiero explicar cada elemento del Ejemplo 1-5 sucesivamente, ya que proporciona una introducción útil a algunos elementos importantes de la sintaxis y la estructura de C#. Lo primero que aparece en este archivo es una declaración de espacio de nombres.

Espacios de nombres

Los espacios de nombres aportan orden y estructura a lo que de otro modo sería un lío horrible. Las bibliotecas de tiempo de ejecución de .NET contienen miles de tipos, y hay muchos más por ahí en paquetes NuGet tanto de Microsoft como de terceros, por no mencionar las clases que escribirás tú mismo. Hay dos problemas que pueden surgir al tratar con tantas entidades con nombre. En primer lugar, resulta difícil garantizar la unicidad. En segundo lugar, puede convertirse en un reto descubrir la API que necesitas; a menos que sepas o puedas adivinar el nombre correcto, es difícil encontrar lo que necesitas en una lista desestructurada de decenas de miles de cosas. Los espacios de nombres resuelven ambos problemas.

La mayoría de los tipos .NET se definen en un espacio de nombres. Hay ciertas convenciones para los espacios de nombres que verás a menudo. Por ejemplo, los tipos de las bibliotecas en tiempo de ejecución de .NET están en espacios de nombres que empiezan por System. Además, Microsoft ha puesto a tu disposición una amplia gama de bibliotecas útiles que no forman parte del núcleo de .NET, y que suelen empezar por Microsoft, o, si son para uso exclusivo con alguna tecnología en particular, pueden llevar ese nombre. (Por ejemplo, hay bibliotecas para utilizar la plataforma en la nube Azure de Microsoft que definen tipos en espacios de nombres que empiezan por Azure.) Las bibliotecas de otros proveedores suelen empezar con el nombre de la empresa o de un producto, mientras que las bibliotecas de código abierto suelen utilizar el nombre de su proyecto. No estás obligado a poner tus propios tipos en espacios de nombres, pero es recomendable que lo hagas. C# no trata System como un espacio de nombres especial, así que nada te impide utilizarlo para tus propios tipos, pero a menos que estés escribiendo una contribución a las bibliotecas de ejecución .NET que vayas aenviar como pull request al repositorio de fuentes de ejecución .NET, entonces es una mala idea porque tenderá a confundir a otros desarrolladores. Deberías elegir algo más distintivo para tu propio código, como el nombre de tu empresa o proyecto. Como puedes ver en la primera línea del Ejemplo 1-5, he elegido definir nuestra clase AverageCalculator dentro de un espacio de nombres llamado Averages, que coincide con el nombre de nuestro proyecto.

El estilo de declaración de espacio de nombres del Ejemplo 1-5 es relativamente nuevo, por lo que es probable que te encuentres con el estilo más antiguo y ligeramente más verboso que se muestra en el Ejemplo 1-6. La diferencia es que la declaración de espacio de nombres va seguida de llaves ({}), y su efecto sólo se aplica al contenido de esas llaves. Esto hace posible que un único archivo contenga varias declaraciones de espacios de nombres. Pero en la práctica, la inmensa mayoría de los archivos C# contienen exactamente una declaración de espacio de nombres. Con la sintaxis antigua, esto significa que la mayor parte del contenido de cada archivo tiene que estar dentro de un par de llaves, con una sangría de un tabulador. El nuevo estilo que se muestra en el Ejemplo 1-5 se aplica a todos los tipos declarados en el archivo sin necesidad de envolverlos explícitamente, lo que reduce el desorden improductivo en nuestros archivos fuente.

Ejemplo 1-6. Declaración de espacio de nombres con ámbito explícito
namespace Averages
{
    public static class AverageCalculator
    {
        ...as before...
    }
}

El espacio de nombres suele dar una pista sobre la finalidad de un tipo. Por ejemplo, todos los tipos de la biblioteca en tiempo de ejecución relacionados con la gestión de archivos se encuentran en el espacio de nombres System.IO, mientras que los relacionados con las redes están en System.Net. Los espacios de nombres pueden formar una jerarquía. El espacio de nombres System de las bibliotecas en tiempo de ejecución contiene tipos y también otros espacios de nombres, como System.Net, y éstos a menudo contienen aún más espacios de nombres, como System.Net.Sockets y System.Net.Mail. Estos ejemplos muestran que los espacios de nombres actúan como una especie de descripción, que puede ayudarte a navegar por la biblioteca. Si buscaras, por ejemplo, el manejo de expresiones regulares, podrías echar un vistazo a los espacios de nombres disponibles y fijarte en el espacio de nombres System.Text. Si miraras allí, encontrarías el espacio de nombres System.Text.RegularExpressions, momento en el que estarías bastante seguro de estar buscando en el lugar correcto.

Los espacios de nombres también proporcionan una forma de garantizar la unicidad. El espacio de nombres en el que se define un tipo forma parte del nombre completo de ese tipo. Esto permite a las bibliotecas utilizar nombres cortos y sencillos para las cosas. Por ejemplo, la API de expresiones regulares incluye una clase Capture que representa los resultados de una captura de expresiones regulares. Si trabajas en software que trata con imágenes, el término captura se utiliza habitualmente para referirse a la adquisición de algunos datos de imagen, y puede que te parezca que Capture es el nombre más descriptivo para una clase de tu propio código. Sería molesto tener que elegir un nombre diferente sólo porque el mejor ya está cogido, sobre todo si tu código de adquisición de imágenes no utiliza expresiones regulares, lo que significa que ni siquiera pensabas utilizar la clase Capture existente.

Pero, de hecho, está bien. Ambas clases pueden llamarse Capture, y seguirán teniendo nombres diferentes. El nombre completo de la clase de expresión regular Capture es efectivamente Sys⁠tem.​Text.RegularExpressions.Capture, y del mismo modo, el nombre completo de tu clase incluiría el espacio de nombres que la contiene (por ejemplo, Spi⁠ffi⁠ngS⁠oft⁠wor⁠ks.Ima⁠gin⁠g.C⁠ap​tu⁠re).

Si realmente quieres, puedes escribir el nombre completo de un tipo cada vez que lo utilices, pero la mayoría de los desarrolladores no quieren hacer algo tan tedioso, que es donde entran las directivas using que puedes ver al principio de los Ejemplos 1-2 y 1-3. Es habitual ver una lista de directivas al principio de cada archivo fuente, indicando los espacios de nombres de los tipos que ese archivo pretende utilizar. Normalmente editarás esta lista para adaptarla a los requisitos de tu archivo. En este ejemplo, la herramienta de línea de comandos dotnet añadió using Microsoft.VisualStudio.TestTools.UnitTesting; cuando creó el proyecto de prueba. Verás conjuntos diferentes en contextos diferentes. Si añades una clase que representa un elemento de interfaz de usuario, por ejemplo, Visual Studio incluiría en la lista varios espacios de nombres relacionados con la interfaz de usuario.

Si un proyecto hace un uso intensivo de un espacio de nombres concreto, podemos evitar tener que poner la misma directiva using en todos y cada uno de los archivos fuente escribiendo una directiva global using directive. Si anteponemos la palabra clave global a la directiva, como en el Ejemplo 1-7, la directiva se aplicará a todos los archivos del proyecto. El SDK .NET lleva esto un paso más allá, generando un archivo oculto en tu proyecto con un conjunto de estas directivas global using para garantizar que los espacios de nombres de uso común, como System y Sy⁠st⁠em.⁠Col⁠lec⁠tio⁠ns.​Gen⁠er⁠ic, estén disponibles. (El conjunto exacto de espacios de nombres añadidos como importaciones globales implícitas varía según el tipo de proyecto: los proyectos web tienen algunos más, por ejemplo. Si te preguntas por qué los proyectos de pruebas unitarias no hacen ya lo que hace el Ejemplo 1-7, es porque el SDK .NET no tiene un tipo de proyecto específico para los proyectos de pruebas: la plantilla mstest que le dijimos a dotnet new que utilizara sólo crea un proyecto ordinario de biblioteca de clases con una referencia a los paquetes de la biblioteca de pruebas unitarias).

Ejemplo 1-7. Una directiva global using
global using Microsoft.VisualStudio.TestTools.UnitTesting;

Con declaraciones using como éstas (ya sean por archivo o globales), puedes utilizar simplemente el nombre corto y no cualificado de una clase. La línea de código que permite al Ejemplo 1-1 hacer su trabajo utiliza la clase System.Console, pero como el SDK añade una directiva implícita global using para el espacio de nombres System, puede referirse a ella simplemente como Console.

Espacios de nombres y nombres de componentes

Anteriormente, utilicé la CLI dotnet para añadir una referencia de nuestro proyectoAverages.Tests a nuestro proyecto Averages. Podrías pensar que las referencias son redundantes: ¿no puede el compilador averiguar qué bibliotecas externas estamos utilizando a partir de los espacios de nombres? Podría hacerlo si hubiera una correspondencia directa entre los espacios de nombres y las bibliotecas opaquetes, pero no la hay.

Los nombres de las bibliotecas a veces coinciden con los espacios de nombres: el popular paquete NuGet Newtonsoft.Json contiene un archivo Newtonsoft.Json.dll que contiene clases en el espacio de nombres Newtonsoft.Json, por ejemplo. Pero se trata de una convención opcional, y a menudo no existe tal conexión: las bibliotecas en tiempo de ejecución de .NET incluyen un archivo System.Private.CoreLib.dll, pero no existe el espacio de nombres System.Private.CoreLib. Así que es necesario decirle al compilador de qué bibliotecas depende tu proyecto, y también qué espacios de nombres utiliza. Veremos la naturaleza y estructura de los archivos de biblioteca con más detalle en el Capítulo 12.

Resolver la ambigüedad

Incluso con los espacios de nombres, existe la posibilidad de ambigüedad. Un mismo archivo fuente puede utilizar dos espacios de nombres que definan una clase con el mismo nombre. Si quieres utilizar esa clase, tendrás que ser explícito, refiriéndote a ella por su nombre completo. Si necesitas utilizar muchas veces esas clases en el archivo, puedes ahorrarte algo de escritura: sólo tendrás que utilizar el nombre completo una vez, porque puedes definir un using alias. El ejemplo 1-8 define dos alias para resolver un conflicto que me he encontrado algunas veces: el marco de interfaz de usuario de escritorio de .NET, la Windows Presentation Foundation (WPF), define una clase Path para trabajar con curvas de Bézier, polígonos y otras formas, pero también hay una clase Path para trabajar con rutas del sistema de archivos, y puede que quieras utilizar ambos tipos juntos para producir una representación gráfica del contenido de un archivo. Añadir simplemente las directivas using para ambos espacios de nombres haría que el simple nombre Path fuera ambiguo si no se calificara. Pero como muestra el Ejemplo 1-8, puedes definir alias distintivos para cada uno.

Ejemplo 1-8. Resolver la ambigüedad con alias
using System.IO;
using System.Windows.Shapes;
using IoPath = System.IO.Path;
using WpfPath = System.Windows.Shapes.Path;

Con estos alias, puedes utilizar IoPath como sinónimo de la clase Path, relacionada con los archivos, y WpfPath para la gráfica.

Por cierto, puedes referirte a tipos de tu propio espacio de nombres sin cualificación, sin necesidad de una directiva using. Por eso el código de prueba del Ejemplo 1-3 no tiene una directiva using Averages;. Sin embargo, puede que te preguntes cómo funciona esto, ya que el código de prueba declara un espacio de nombres diferente, Averages.Tests. Para entenderlo, tenemos que fijarnos en la anidación de espacios de nombres.

Espacios de nombres anidados

Como ya has visto, las bibliotecas en tiempo de ejecución de .NET anidan sus espacios de nombres, a veces de forma bastante extensa, y a menudo querrás hacer lo mismo. Hay dos formas de hacerlo. Puedes anidar declaraciones de espacios de nombres, como muestra el Ejemplo 1-9.

Ejemplo 1-9. Anidamiento de declaraciones de espacios de nombres
namespace MyApp
{
    namespace Storage
    {
        ...
    }
}

También puedes especificar el espacio de nombres completo en una sola declaración, como muestra el Ejemplo 1-10. Este es el estilo más utilizado. Este estilo de declaración única funciona tanto con el nuevo tipo de declaración que se muestra en el Ejemplo 1-10 como con el estilo más antiguo que utiliza llaves.

Ejemplo 1-10. Espacio de nombres anidado con una sola declaración
namespace MyApp.Storage;

Cualquier código que escribas en un espacio de nombres anidado podrá utilizar tipos no sólo de ese espacio de nombres, sino también de los espacios de nombres que lo contengan, sin necesidad de cualificación. El código de los Ejemplos 1-9 o 1-10 no necesitaría cualificación explícita ni directivas using para utilizar tipos del espacio de nombres MyApp.Storage o del espacio de nombres MyApp. Por eso en el Ejemplo 1-3 no necesité añadir una directiva using Averages; para poder acceder a AverageCalculator en el espacio de nombres Averages: la prueba se declaró en el espacio de nombres Averages.Tests, y como éste está anidado en el espacio de nombres Averages, el código tiene acceso automáticamente a ese espacio de nombres externo.

Cuando defines espacios de nombres anidados, la convención es crear una jerarquía de directorios coincidente. Algunas herramientas esperan esto. Aunque VS Code no tiene actualmente ninguna expectativa particular al respecto, Visual Studio sí sigue esta convención. Si tu proyecto se llama MyApp, colocará las clases nuevas en el espacio de nombres MyApp cuando las añadas al proyecto. Pero si creas un nuevo directorio en el proyecto llamado, por ejemplo, Almacenamiento, Visual Studio pondrá las nuevas clases que crees en ese directorio en el espacio de nombres MyApp.Storage. De nuevo, no estás obligado a mantenerlo: Visual Studio sólo añade una declaración de espacio de nombres al crear el archivo, y eres libre de cambiarla. El compilador no necesita que el espacio de nombres coincida con tu jerarquía de directorios. Pero como la convención es compatible con varias herramientas, incluido Visual Studio, la vida será más fácil si la sigues.

Clases

Después de la declaración del espacio de nombres, nuestro archivo CalculadoraPromedio.cs define una clase. El Ejemplo 1-11 muestra esta parte del archivo. Comienza con la palabra clave public, que permite que otros componentes puedan acceder a esta clase. A continuación aparece la palabra clave static, que indica que esta clase no está pensada para ser instanciada: sólo ofrece operaciones a nivel de clase y no funciones por instancia. Luego viene la palabra clave class seguida del nombre, y por supuesto el nombre completo del tipo es efectivamente Averages.AverageCalculator, debido a la declaración del espacio de nombres. Como puedes ver, C# utiliza llaves ({}) para delimitar todo tipo de cosas: ya lo vimos en la antigua (pero aún muy utilizada) sintaxis de declaración del espacio de nombres, y aquí puedes ver lo mismo con la clase, así como con el método que contiene.

Ejemplo 1-11. Una clase con un método
public static class AverageCalculator
{
    public static double ArithmeticMean(string[] args)
    {
        return 1.0;
    }
}

Las clases son el mecanismo de C# para definir entidades que combinan estado y comportamiento, un modismo común orientado a objetos. Pero esta clase no contiene más que un único método. C# no admite métodos globales: todo el código debe escribirse como miembro de algún tipo. Así que esta clase en concreto no es muy interesante: su única función es servir de contenedor para el método que hará el trabajo real. Veremos otros usos interesantes de las clases en el Capítulo 3.

Al igual que con la clase, he marcado el método como public para permitir el acceso desde otros componentes. También he declarado que se trata de un método estático, lo que significa que no es necesario crear una instancia del tipo que lo contiene (AverageCalculator, en este caso) para invocar el método. La palabra clave double que sigue indica que el tipo de datos que devuelve este método es un número de coma flotante de doble precisión.

La declaración del método va seguida del cuerpo del método, que en este ejemplo contiene código que devuelve un valor de marcador de posición, por lo que sólo queda modificar el código dentro de las llaves que delimitan el cuerpo del método. El Ejemplo 1-12 muestra código que calcula la media en lugar de devolver simplemente 1,0.

Ejemplo 1-12. Cálculo de la media
return args.Select(numText => double.Parse(numText)).Average();

Esto se basa en funciones de biblioteca para trabajar con colecciones que forman parte del conjunto de funciones conocidas colectivamente como LINQ, que es el tema del Capítulo 10. Pero para describir rápidamente lo que ocurre aquí, el método Select nos permite aplicar una operación a cada elemento de una colección, y en este caso, la operación que estoy aplicando es el método double.Parse, una función de la biblioteca en tiempo de ejecución .NET que convierte una cadena textual que contiene un número en el tipo nativo de punto flotante de doble precisión. Y luego empujamos estos resultados transformados a través del método Average de LINQ, que realiza el cálculo por nosotros.

Con esto en su sitio, si vuelvo a ejecutar mis pruebas, todas pasarán. Así que aparentemente el código funciona. Sin embargo, veo un problema si intento verificarlo informalmente ejecutando el programa, lo que puedo hacer con este comando:

./Averages/bin/Debug/net8.0/Averages 1 2 3 4 5

Esto sólo escribe Hello, World! en la pantalla. He escrito y probado el código que realiza el cálculo necesario, pero aún no lo he conectado al punto de entrada del programa. El código que se ejecuta cuando se inicia el programa está en Program.cs, aunque ese nombre de archivo no tiene nada de especial. El punto de entrada del programa puede estar en cualquier archivo. En versiones anteriores de C#, indicabas el punto de entrada definiendo un método static llamado Main, como se muestra en el Ejemplo 1-2, y aún puedes hacerlo con C# 12.0, pero normalmente utilizamos el enfoque más nuevo y sucinto: escribimos un archivo que contenga sentencias ejecutables sin ponerlas explícitamente dentro de un método de un tipo, y el compilador de C# tratará eso como el punto de entrada. (Sólo puedes tener un archivo en tu proyecto escrito de esa forma, porque tu programa sólo puede tener un punto de entrada). Si sustituyo todo el contenido de Programa.cs por el código que se muestra en el Ejemplo 1-13, tendrá el efecto deseado.

Ejemplo 1-13. Punto de entrada del programa con argumentos
using Averages;

Console.WriteLine(AverageCalculator.ArithmeticMean(args));

Fíjate en que he tenido que añadir una directiva using: cuando utilizas esta sintaxis de punto de entrada de programa despojado, el código de ese archivo no está por defecto en ningún espacio de nombres, así que tengo que indicar que quiero utilizar la clase que definí en el espacio de nombres Averages. Después, este código invoca el método que escribí antes, pasando args como argumento, y luego llama a Console.WriteLine para mostrar el resultado. Cuando utilizas este estilo de punto de entrada al programa, args tiene un nombre especial: es, en realidad, una variable local definida implícitamente que proporciona acceso a los argumentos de la línea de comandos. Será una matriz de cadenas, con una entrada por cada argumento. Si quieres volver a ejecutar el programa con los mismos argumentos que antes, ejecuta primero el comando dotnet build para reconstruirlo.

Consejo

Algunos lenguajes de la familia C incluyen el nombre de archivo del propio programa como primer argumento, basándose en que forma parte de lo que el usuario escribió en el símbolo del sistema. C# no sigue esta convención. Si el programa se lanza sin argumentos, la longitud de la matriz será 0. Habrás observado que el código no se las arregla bien con eso. Siéntete libre de añadir un nuevo escenario de prueba que defina el comportamiento relevante, y de modificar el programa para que coincida.

Pruebas unitarias

Ahora que el programa funciona, quiero volver a las pruebas, porque ilustran una característica de C# que el programa principal no tiene. Si vuelves al Ejemplo 1-3, comienza de una forma bastante ordinaria: tenemos una directiva using y luego una declaración de espacio de nombres, para Averages.Tests esta vez, que coincide con el nombre del proyecto de pruebas. Pero la clase tiene un aspecto diferente. El Ejemplo 1-14 muestra la parte relevante del Ejemplo 1-3.

Ejemplo 1-14. Clase de prueba con atributo
[TestClass]
public class WhenCalculatingAverages
{

Inmediatamente antes de la declaración de la clase aparece el texto [TestClass]. Se trata de un atributo. Los atributos son anotaciones que puedes aplicar a clases, métodos y otras características del código. La mayoría de ellos no hacen nada por sí mismos: el compilador registra el hecho de que el atributo está presente en la salida compilada, pero eso es todo. Los atributos sólo son útiles cuando algo los busca, por lo que suelen ser utilizados por los frameworks. En este caso, estoy utilizando el marco de pruebas unitarias de Microsoft, que busca clases anotadas con este atributo TestClass. Ignorará las clases que no tengan esta anotación. Los atributos suelen ser específicos de un marco de trabajo concreto, y puedes definir los tuyos propios, como veremos en el Capítulo 14.

Los dos métodos de la clase también están anotados con atributos. El Ejemplo 1-15 muestra los fragmentos correspondientes del Ejemplo 1-3. El ejecutor de pruebas ejecutará cualquier método marcado con el atributo [TestMethod].

Ejemplo 1-15. Métodos anotados
[TestMethod]
public void SingleInputShouldProduceSameValueAsResult()
...

[TestMethod]
public void MultipleInputsShouldProduceAverageAsResult()
...

Y con eso, hemos examinado cada elemento de un programa y el proyecto de prueba que verifica que funciona según lo previsto.

Resumen

Ya has visto la estructura básica de los programas en C#. Creé una solución que contenía dos proyectos, uno para las pruebas y otro para el programa propiamente dicho. Se trataba de un ejemplo sencillo, por lo que cada proyecto sólo tenía uno o dos archivos fuente de interés. Cuando era necesario, estos archivos comenzaban con directivas using que indicaban los tipos que utilizaba el archivo. El punto de entrada del programa utilizaba un estilo despojado, pero los otros dos utilizaban una estructura más convencional, que contenía una declaración de espacio de nombres que indicaba el espacio de nombres que poblaba el archivo, y una clase que contenía uno o varios métodos u otros miembros, como campos.

Veremos los tipos y sus miembros con mucho más detalle en el Capítulo 3, pero antes, el Capítulo 2 tratará del código que vive dentro de los métodos, donde expresamos lo que queremos que hagan nuestros programas.

1 La AOT nativa, por tanto, no puede ofrecer compilación por niveles.

2 Las versiones anteriores del tiempo de ejecución que ahora llamamos .NET se llamaban .NET Core, y cuando se renombró como .NET a secas, pasó de la 3.1 a la 5.0 para enfatizar el alejamiento de .NET Framework, cuya última versión es la 4.8.1.

Get Programación C# 12 now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.