Tema 2:
Introducción a C#
(C) 2001 José Antonio González Seco
Origen y necesidad de un
nuevo lenguaje
C# (leído en inglés "C Sharp" y en
español "C Almohadilla") es el nuevo lenguaje de
propósito general diseñado por Microsoft para su
plataforma .NET. Sus principales creadores son Scott Wiltamuth y
Anders Hejlsberg, éste último también conocido
por haber sido el diseñador del lenguaje Turbo Pascal y la
herramienta RAD Delphi.
Aunque es posible escribir código para la plataforma .NET en
muchos otros lenguajes, C# es el único que ha sido
diseñado específicamente para ser utilizado en ella,
por lo que programarla usando C# es mucho más sencillo e
intuitivo que hacerlo con cualquiera de los otros lenguajes ya que
C# carece de elementos heredados innecesarios en .NET. Por esta
razón, se suele decir que C# es el lenguaje nativo de
.NET
La sintaxis y estructuración de C# es muy similar a la C++,
ya que la intención de Microsoft con C# es facilitar la
migración de códigos escritos en estos lenguajes a C#
y facilitar su aprendizaje a los desarrolladores habituados a
ellos. Sin embargo, su sencillez y el alto nivel de productividad
son equiparables a los de Visual Basic.
Un lenguaje que hubiese sido ideal utilizar para estos menesteres
es Java, pero debido a problemas con la empresa creadora del mismo
-Sun-, Microsoft ha tenido que desarrollar un nuevo lenguaje que
añadiese a las ya probadas virtudes de Java las
modificaciones que Microsoft tenía pensado añadirle
para mejorarlo aún más y hacerlo un lenguaje
orientado al desarrollo de componentes.
En resumen, C# es un lenguaje de programación que toma las
mejores características de lenguajes preexistentes como
Visual Basic, Java o C++ y las combina en uno solo. El hecho de ser
relativamente reciente no implica que sea inmaduro, pues Microsoft
ha escrito la mayor parte de la BCL usándolo, por lo que su
compilador es el más depurado y optimizado de los incluidos
en el .NET Framework SDK
Características
de C#
Con la idea de que los programadores más experimentados
puedan obtener una visión general del lenguaje, a
continuación se recoge de manera resumida las principales
características de C# Alguna de las características
aquí señaladas no son exactamente propias del
lenguaje sino de la plataforma .NET en general. Sin embargo,
también se comentan aquí también en tanto que
tienen repercusión directa en el lenguaje, aunque se
indicará explícitamente cuáles son este tipo
de características cada vez que se toquen:
-
Sencillez: C# elimina muchos elementos que otros
lenguajes incluyen y que son innecesarios en .NET. Por ejemplo:
-
El código escrito en C# es autocontenido, lo
que significa que no necesita de ficheros adicionales al
propio fuente tales como ficheros de cabecera o ficheros IDL
-
El tamaño de los tipos de datos básicos es fijo
e independiente del compilador, sistema operativo o
máquina para quienes se compile (no como en
C++), lo que facilita la portabilidad del código.
-
No se incluyen elementos poco útiles de lenguajes como
C++ tales como macros, herencia múltiple o la
necesidad de un operador diferente del punto (.)
acceder a miembros de espacios de nombres (::)
-
Modernidad: C# incorpora en el propio lenguaje elementos
que a lo largo de los años ha ido demostrándose
son muy útiles para el desarrollo de aplicaciones y que
en otros lenguajes como Java o C++ hay que simular, como un tipo
básico decimal que permita realizar operaciones de
alta precisión con reales de 128 bits (muy útil en
el mundo financiero), la inclusión de una
instrucción foreach que permita recorrer
colecciones con facilidad y es ampliable a tipos definidos por
el usuario, la inclusión de un tipo básico
string para representar cadenas o la distinción de
un tipo bool específico para representar valores
lógicos.
-
Orientación a objetos: Como todo lenguaje de
programación de propósito general
actual, C# es un lenguaje orientado a objetos, aunque eso es
más bien una característica del CTS que de C#. Una
diferencia de este enfoque orientado a objetos respecto al de
otros lenguajes como C++ es que el de C# es más puro en
tanto que no admiten ni funciones ni variables globales sino que
todo el código y datos han de definirse dentro de
definiciones de tipos de datos, lo que reduce problemas por
conflictos de nombres y facilita la legibilidad del
código.
C# soporta todas las características propias del paradigma
de programación orientada a objetos:
encapsulación, herencia y polimorfismo.
En lo referente a la encapsulación es importante
señalar que aparte de los típicos
modificadores
public, private y protected, C# añade un
cuarto modificador llamado internal, que puede combinarse
con protected e indica que al elemento a cuya
definición precede sólo puede accederse desde su
mismo ensamblado.
Respecto a la herencia -a diferencia de C++ y al igual que Java- C#
sólo admite herencia simple de clases ya que la
múltiple provoca más quebraderos de cabeza que
facilidades y en la mayoría de los casos su utilidad puede
ser simulada con facilidad mediante herencia múltiple de
interfaces. De todos modos, esto vuelve a ser más bien una
característica propia del CTS que de C#.
Por otro lado y a diferencia de Java, en C# se ha optado por hacer
que todos los métodos sean por defecto
sellados y que los redefinibles hayan
de
marcarse con el modificador virtual (como en C++), lo que
permite evitar errores derivados de redefiniciones accidentales.
Además, un efecto secundario de
esto es que las llamadas a los métodos serán
más eficientes por defecto al no tenerse que buscar en la
tabla de funciones virtuales la implementación de los mismos
a la que se ha de llamar. Otro efecto secundario es que permite que
las llamadas a los métodos virtuales se puedan hacer
más eficientemente al contribuir a que el tamaño de
dicha tabla se reduzca.
-
Orientación a componentes: La propia sintaxis de
C# incluye elementos propios del diseño de componentes
que otros lenguajes tienen que simular mediante construcciones
más o menos complejas. Es decir, la sintaxis de C#
permite definir cómodamente propiedades (similares
a campos de acceso controlado), eventos
(asociación controlada de funciones de respuesta a
notificaciones) o atributos (información sobre un
tipo o sus miembros)
-
Gestión automática de memoria: Como ya se
comentó, todo lenguaje de .NET tiene a su
disposición el recolector de basura del CLR. Esto tiene
el efecto en el lenguaje de que no es necesario incluir
instrucciones de destrucción de objetos. Sin embargo,
dado que la destrucción de los objetos a través
del recolector de basura es indeterminista y sólo se
realiza cuando éste se active -ya sea por falta de
memoria, finalización de la aplicación o solicitud
explícita en el fuente-, C# también proporciona un
mecanismo de liberación de recursos determinista a
través de la instrucción using.
-
Seguridad de tipos: C# incluye mecanismos que permiten
asegurar que los accesos a tipos de datos siempre se realicen
correctamente, lo que permite evita que se produzcan errores
difíciles de detectar por acceso a memoria no
perteneciente a ningún objeto y es especialmente
necesario en un entorno gestionado por un recolector de basura.
Para ello se toman medidas del tipo:
-
Sólo se admiten conversiones entre tipos
compatibles. Esto es, entre un tipo y antecesores suyos,
entre tipos para los que explícitamente se haya
definido un operador de conversión, y entre un tipo y
un tipo hijo suyo del que un objeto del primero almacenase
una referencia del segundo (downcasting) Obviamente,
lo último sólo puede comprobarlo en tiempo de
ejecución el CLR y no el compilador, por lo que en
realidad el CLR y el compilador colaboran para asegurar la
corrección de las conversiones.
-
No se pueden usar variables no inicializadas. El
compilador da a los campos un valor por defecto consistente
en ponerlos a cero y controla mediante análisis del
flujo de control del fuente que no se lea ninguna variable
local sin que se le haya asignado previamente algún
valor.
-
Se comprueba que todo acceso a los elementos de una
tabla se realice con índices que se encuentren
dentro del rango de la misma.
-
Se puede controlar la producción de
desbordamientos en operaciones aritméticas,
informándose de ello con una excepción cuando
ocurra. Sin embargo, para conseguirse un mayor rendimiento en
la aritmética estas comprobaciones no se hacen por
defecto al operar con variables sino sólo con
constantes (se pueden detectar en tiempo de
compilación)
-
A diferencia de Java, C# incluye delegados, que son
similares a los punteros a funciones de C++ pero siguen un
enfoque orientado a objetos, pueden almacenar
referencias a varios métodos simultáneamente, y
se comprueba que los métodos a los que apunten tengan
parámetros y valor de retorno del tipo indicado al
definirlos.
-
Pueden definirse métodos que admitan un número
indefinido de parámetros de un cierto tipo, y a
diferencia lenguajes como C/C++, en C# siempre se comprueba
que los valores que se les pasen en cada llamada sean de los
tipos apropiados.
-
Instrucciones seguras: Para evitar errores muy comunes,
en C# se han impuesto una serie de restricciones en el uso de
las instrucciones de control más comunes. Por ejemplo, la
guarda de toda condición ha de ser una expresión
condicional y no aritmética, con lo que se evitan errores
por confusión del operador de igualdad (==) con el
de asignación (=); y todo caso de un switch
ha de terminar en un break o goto que indique
cuál es la siguiente acción a realizar, lo que
evita la ejecución accidental de casos y facilita su
reordenación.
-
Sistema de tipos unificado: A diferencia de C++, en C#
todos los tipos de datos que se definan siempre
derivarán, aunque sea de manera implícita, de una
clase base común llamada System.Object, por lo que
dispondrán de todos los miembros definidos en ésta
clase (es decir, serán "objetos")
A diferencia de Java, en C# esto también es aplicable a los
tipos de datos básicos
Además, para conseguir que ello no tenga una
repercusión negativa en su nivel de rendimiento, se ha
incluido un mecanismo transparente de boxing y
unboxing con el que se consigue que sólo sean
tratados como objetos cuando la situación lo requiera,
y mientras tanto puede aplicárseles optimizaciones
específicas.
El hecho de que todos los tipos del lenguaje deriven de una clase
común facilita enormemente el diseño de colecciones
genéricas que puedan almacenar objetos de cualquier tipo.
-
Extensibilidad de tipos básicos: C# permite
definir, a través de estructuras, tipos de datos
para los que se apliquen las mismas optimizaciones que para los
tipos de datos básicos. Es decir, que se puedan almacenar
directamente en pila (luego su creación,
destrucción y acceso serán más
rápidos) y se asignen por valor y no por referencia. Para
conseguir que lo último no tenga efectos negativos al
pasar estructuras como parámetros de métodos, se
da la posibilidad de pasar referencias a pila a través
del modificador de parámetro ref.
-
Extensibilidad de operadores: Para facilitar la
legibilidad del código y conseguir que los nuevos tipos
de datos básicos que se definan a través de las
estructuras estén al mismo nivel que los básicos
predefinidos en el lenguaje, al igual que C++ y a diferencia de
Java, C# permite redefinir el significado de la mayoría
de los operadores -incluidos los de conversión, tanto
para conversiones implícitas como explícitas-
cuando se apliquen a diferentes tipos de objetos.
Las redefiniciones de operadores se hacen de manera inteligente, de
modo que a partir de una única definición de los
operadores ++ y -- el compilador puede deducir
automáticamente como ejecutarlos de manera prefijas y
postifja; y definiendo operadores simples (como +), el
compilador deduce cómo aplicar su versión de
asignación compuesta (+=) Además, para asegurar la
consistencia, el compilador vigila que los operadores con opuesto
siempre se redefinan por parejas (por ejemplo, si se redefine
==, también hay que redefinir !=)
También se da la posibilidad, a través del concepto
de indizador, de redefinir el significado del operador
[] para los tipos de dato definidos por el usuario, con lo
que se consigue que se pueda acceder al mismo como si fuese una
tabla. Esto es muy útil para trabajar con tipos que
actúen como colecciones de objetos.
Extensibilidad de modificadores: C# ofrece, a través
del concepto de atributos, la posibilidad de añadir a
los metadatos del módulo resultante de la compilación
de cualquier fuente información adicional a la generada por
el compilador que luego podrá ser consultada en
tiempo ejecución a través de la librería de
reflexión de .NET . Esto, que más bien es una
característica propia de la plataforma .NET y no de C#,
puede usarse como un mecanismo para definir nuevos modificadores.
-
Versionable: C# incluye una política de
versionado que permite crear nuevas versiones de tipos sin
temor a que la introducción de nuevos miembros provoquen
errores difíciles de detectar en tipos hijos previamente
desarrollados y ya extendidos con miembros de igual nombre a los
recién introducidos.
Si una clase introduce un nuevo método cuyas redefiniciones
deban seguir la regla de llamar a la versión de su padre en
algún punto de su código, difícilmente
seguirían esta regla miembros de su misma signatura
definidos en clases hijas previamente a la definición del
mismo en la clase padre; o si introduce un nuevo campo con el
mismo nombre que algún método de una clase
hija, la clase hija dejará de funcionar. Para evitar que
esto ocurra, en C# se toman dos medidas:
-
Se obliga a que toda redefinición deba incluir
el modificador override, con lo que la versión
de la clase hija nunca sería considerada como una
redefinición de la versión de miembro en la
clase padre ya que no incluiría override. Para
evitar que por accidente un programador incluya este
modificador, sólo se permite incluirlo en miembros que
tengan la misma signatura que miembros marcados como
redefinibles mediante el modificador virtual.
Así además se evita el error tan frecuente en
Java de creerse haber redefinido un miembro, pues si el
miembro con override no existe en la clase padre se
producirá un error de compilación.
-
Si no se considera redefinición, entonces se considera
que lo que se desea es ocultar el método de la clase
padre, de modo que para la clase hija sea como si nunca
hubiese existido. El compilador avisará de esta
decisión a través de un mensaje de aviso que
puede suprimirse incluyendo el modificador new
en la definición del miembro en la clase hija para
así indicarle explícitamente la
intención de ocultación.
-
Eficiente: En principio, en C# todo el código
incluye numerosas restricciones para asegurar su seguridad y no
permite el uso de punteros. Sin embargo, y a diferencia de Java,
en C# es posible saltarse dichas restricciones manipulando
objetos a través de punteros. Para ello basta marcar
regiones de código como inseguras (modificador
unsafe) y podrán usarse en ellas punteros de forma
similar a cómo se hace en C++, lo que puede resultar
vital para situaciones donde se necesite una eficiencia y
velocidad procesamiento muy grandes.
-
Compatible: Para facilitar la migración de
programadores, C# no sólo mantiene una sintaxis muy
similar a C, C++ o Java que permite incluir directamente
en código escrito en C# fragmentos de código
escrito en estos lenguajes, sino que el CLR también
ofrece, a través de los llamados Platform Invocation
Services (PInvoke), la posibilidad de acceder a
código nativo escrito como funciones sueltas no
orientadas a objetos tales como las DLLs de la API Win32.
Nótese que la capacidad de usar punteros en código
inseguro permite que se pueda acceder con facilidad a este
tipo de funciones, ya que éstas muchas veces esperan
recibir o devuelven punteros.
También es posible acceder desde código escrito en C#
a objetos COM. Para facilitar esto, el .NET Framework SDK
incluye una herramientas llamadas tlbimp y
regasm mediante las que es posible generar
automáticamente clases proxy que permitan, respectivamente,
usar objetos COM desde .NET como si de objetos .NET se tratase y
registrar objetos .NET para su uso desde COM.
Finalmente, también se da la posibilidad de usar controles
ActiveX desde código .NET y viceversa. Para lo primero se
utiliza la utilidad aximp, mientras que para lo segundo se
usa la ya mencionada regasm.
Escritura de
aplicaciones
Aplicación
básica ¡Hola Mundo!
Básicamente una aplicación en C# puede verse
como un conjunto de uno o más ficheros de código
fuente con las instrucciones necesarias para que la
aplicación funcione como se desea y que son pasados al
compilador para que genere un ejecutable. Cada uno de estos
ficheros no es más que un fichero de texto plano escrito
usando caracteres Unicode y siguiendo la sintaxis propia de C#.
Como primer contacto con el lenguaje, nada mejor que el
típico programa de iniciación "¡Hola
Mundo!" que lo único que hace al ejecutarse es mostrar
por pantalla el mensaje ¡Hola Mundo! Su código es:
1: class HolaMundo
2: {
3: static void Main()
4: {
5: System.Console.WriteLine("¡Hola Mundo!");
6: }
7: }
Todo el código escrito en C# se ha de escribir dentro de una
definición de clase, y lo que en la línea 1:
se dice es que se va a definir una clase (class) de nombre
HolaMundo1 cuya definición estará comprendida entre
la llave de apertura de la línea 2: y su
correspondiente llave de cierre en la línea
línea 7:
Dentro de la definición de la clase (línea 3:)
se define un método de nombre Main cuyo código es el
indicado entre la llave de apertura de la línea 4: y
su respectiva llave de cierre (línea 6:) Un
método no es más que un conjunto de instrucciones a
las que se les asocia un nombre, de modo que para posteriormente
ejecutarlas baste referenciarlas por su nombre en vez de tener que
reescribirlas.
La partícula que antecede al nombre del método indica
cuál es el tipo de valor que se devuelve tras la
ejecución del método, y en este caso es void
que significa que no se devuelve nada. Por su parte, los
paréntesis que se colocado tras el nombre del método
indican cuáles son los parámetros éste toma, y
como en este caso están vacíos ello significa que el
método no toma parámetros. Los parámetros de
un método permiten variar el resultado de su
ejecución según los valores que se les dé en
cada llamada.
La palabra static que antecede a la declaración del
tipo de valor devuelto es un modificador del
significado de la declaración de método que indica
que el método está asociado a la clase dentro
de la que se define y no a los objetos que se creen a partir de
ella. Main() es lo que es denomina el punto de entrada de la
aplicación, que no es más que el método por el
que comenzará su ejecución. Necesita del modificador
static para evitar que para llamarlo haya que crear
algún objeto de la clase donde se haya definido.
Finalmente, la línea 5: contiene la
instrucción con el código a ejecutar, que lo que
se hace es solicitar la ejecución del método
WriteLine() de la clase Console definida en el
espacio de nombres System pasándole como
parámetro la cadena de texto con el contenido ¡Hola
Mundo! Nótese que las cadenas de textos son secuencias
de caracteres delimitadas por comillas dobles aunque dichas
comillas no forman parte de la cadena. Por su parte, un espacio de
nombres puede considerarse que es algo similar para las clases a lo
que un directorio es para los ficheros; es decir, es una forma de
agruparlas.
El método WriteLine() se usará muy a menudo en
los próximos temas, por lo que es conveniente señalar
ahora que una forma de llamarlo que se utilizará en
repetidas ocasiones consiste en pasarle un número indefinido
de otros parámetros de cualquier tipo e incluir en el
primero subcadenas de la forma {i}. Con ello se consigue que
se muestre por la ventana de consola la cadena que se le pasa como
primer parámetro pero sustituyéndole las subcadenas
{i} por el valor convertido en cadena de texto del parámetro
que ocupe la posición i+2 en la llamada a
WriteLine(). Por ejemplo, la siguiente instrucción
mostraría Tengo 5 años por pantalla si x valiese 5:
System.Console.WriteLine("Tengo {0} años", x);
Para indicar cómo convertir cada objeto en un cadena de
texto basta redefinir su método ToString(), aunque
esto es algo que no se verá hasta el Tema 5:
Clases.
Antes de seguir es importante resaltar que C# es sensible a las
mayúsculas, los que significa que no da igual la
capitalización con la que se escriban los identificadores.
Es decir, no es lo mismo escribir Console que COnsole o CONSOLE, y
si se hace de alguna de las dos últimas formas el compilador
producirá un error debido a que en el espacio de nombres
System no existe ninguna clase con dichos nombres. En este
sentido, cabe señalar que un error común entre
programadores acostumbrados a Java es llamar al punto de entrada
main en vez de Main, lo que provoca un error al compilar
ejecutables en tanto que el compilador no detectará ninguna
definición de punto de entrada.
Puntos de entrada
Ya se ha dicho que el punto de entrada de una
aplicación es un método de nombre Main que
contendrá el código por donde se ha de iniciar la
ejecución de la misma. Hasta ahora sólo se ha visto
una versión de Main() que no toma parámetros y tiene
como tipo de retorno void, pero en realidad todas sus
posibles versiones son:
static void Main()
static int Main()
static int Main(string[] args)
static void Main(string[] args)
Como se ve, hay versiones de Main() que devuelven un valor de tipo
int. Un int no es más que un tipo de datos
capaz de almacenar valor enteros comprendidos entre
-2.1471483.648 y 2.1471483.647, y el
número devuelto por Main() sería interpretado como
código de retorno de la aplicación. Éste valor
suele usarse para indicar si la aplicación a terminado con
éxito (generalmente valor 0) o no (valor según la
causa de la terminación anormal), y en el Tema 8:
Métodos se explicará como devolver valores.
También hay versiones de Main() que toman un
parámetro donde se almacenará la lista de argumentos
con los que se llamó a la aplicación, por lo que
sólo es útil usar estas versiones del punto de
entrada si la aplicación va a utilizar dichos argumentos
para algo. El tipo de este parámetro es string[], lo
que significa que es una tabla de cadenas de texto (en el Tema
5: Campos se explicará detenidamente qué son las
tablas y las cadenas), y su nombre -que es el que habrá de
usarse dentro del código de Main() para hacerle referencia-
es args en el ejemplo, aunque podría dársele
cualquier otro
Compilación en
línea de comandos
Una vez escrito el código anterior con algún editor
de textos -como el Bloc de Notas de Windows- y
almacenado en formato de texto plano en un fichero HolaMundo.cs, para
compilarlo basta abrir una ventana de consola (MS-DOS en Windows),
colocarse en el directorio donde se encuentre y pasárselo
como parámetro al compilador así:
csc HolaMundo.cs
csc.exe es el compilador de C# incluido en el .NET Framework
SDK para Windows de Microsoft, y es posible llamarlo desde
cualquier directorio en tanto que al instalarlo se añade una
referencia al mismo en el path. Si utiliza otros
compiladores de C# puede que varie la forma en que se realice
la compilación, por lo que lo que aquí se
explica en principio sólo podría ser
válido para el compilador de Microsoft para Windows.
Tras la compilación se obtendría un ejecutable
llamado HolaMundo.exe cuya ejecución produciría la
siguiente salida por la ventana de consola:
¡Hola Mundo!
Si la aplicación que se vaya a compilar no utilizase la
ventana de consola para mostrar su salida sino una interfaz
gráfica de ventanas, entonces habría que compilarla
pasando al compilador la opción /t con el valor
winexe antes del nombre del fichero a compilar. Si no se
hiciese así se abríría la ventana de consola
cada vez que ejecutase la aplicación de ventanas, lo que
suele ser indeseable en este tipo de aplicaciones. Así, para
compilar Ventanas.cs como ejecutable de ventanas sería
conveniente escribir:
csc /t:winexe Ventanas.cs
Nótese que aunque el nombre winexe dé la
sensación de que este valor para la opción /t
sólo permite generar ejecutables de ventanas, en realidad lo
que permite es generar ejecutables sin ventana de consola asociada.
Por tanto, también puede usarse para generar ejecutables que
no tengan ninguna interfaz asociada, ni de consola ni
gráfica.
Si en lugar de un ejecutable -ya sea de consola o de ventanas- se
desea obtener una librería, entonces al compilar hay que
pasar al compilador la opción /t con el valor
library. Por ejemplo, siguiendo con el ejemplo inicial
habría que escribir:
csc /t:library HolaMundo.cs
En este caso se generaría un fichero HolaMundo.dll cuyos
tipos de datos podrían utilizarse desde otros fuentes
pasando al compilador una referencia a los mismos mediante la
opción /r. Por ejemplo, para compilar como ejecutable
un fuente A.cs que use la clase HolaMundo de la librería
HolaMundo.dll se escribiría:
csc /r:HolaMundo.dll A.cs
En general /r permite referenciar a tipos definidos en
cualquier ensamblado, por lo que el valor que se le indique
también puede ser el nombre de un ejecutable. Además,
en cada compilación es posible referenciar múltiples
ensamblados ya sea incluiyendo la opción /r una vez
por cada uno o incluiyendo múltiples referencias en una
única opción /r usando comas o puntos y comas
como separadores. Por ejemplo, las siguientes tres llamadas al
compilador son equivalentes:
csc /r:HolaMundo.dll;Otro.dll;OtroMás.exe A.cs
csc /r:HolaMundo.dll,Otro.dll,OtroMás.exe A.cs
csc /t:HolaMundo.dll /r:Otro.dll /r:OtroMás.exe A.cs
Hay que señalar que aunque no se indique nada, en toda
compilación siempre se referencia por defecto a la
librería mscorlib.dll de la BCL, que incluye los
tipos de uso más frecuente. Si se usan tipos de la BCL no
incluidos en ella habrá que incluir al compilar referencias
a las librerías donde estén definidos (en la
documentación del SDK sobre cada tipo de la BCL puede
encontrar información sobre donde se definió)
Tanto las librerías como los ejecutables son ensamblados.
Para generar un módulo de código que no forme parte
de ningún ensamblado sino que contenga definiciones de tipos
que puedan añadirse a ensamblados que se compilen
posteriormente, el valor que ha de darse al compilar a la
opción /t es module. Por ejemplo:
csc /t:module HolaMundo.cs
Con la instrucción anterior se generaría un
módulo llamado HolaMundo.netmodule que podría
ser añadido a compilaciones de ensamblados
incluyéndolo como valor de la opción
/addmodule. Por ejemplo, para añadir el
módulo anterior a la compilación del fuente
librería Lib.cs como librería se escribiría:
csc /t:library /addmodule:HolaMundo.netmodule Lib.cs
Aunque hasta ahora todas las compilaciones de ejemplo se han
realizado utilizando un único fichero de código
fuente, en realidad nada impide que se puedan utilizar más.
Por ejemplo, para compilar los ficheros A.cs y B.cs en una
librería A.dll se ejecutaría:
csc /t:library A.cs B.cs
Nótese que el nombre que por defecto se dé al
ejecutable generado siempre es igual al del primer fuente
especificado pero con la extensión propia del tipo de
compilación realizada (.exe para ejecutables,
.dll para librerías y .netmodule para
módulos) Sin embargo, puede especificárse como
valor en la opción /out del compilador cualquier otro
tal y como muestra el siguiente ejemplo que compila el fichero A.cs
como una librería de nombre Lib.exe:
csc /t:library /out:Lib.exe A.cs
Véase que aunque se haya dado un nombre terminado en
.exe al fichero resultante, éste sigue siendo una
librería y no un ejecutable e intentar ejecutarlo
produciría un mensaje de error. Obviamente no tiene mucho
sentido darle esa extensión, y sólo se le ha dado en
este ejemplo para demostrar que, aunque recomendable, la
extensión del fichero no tiene porqué corresponderse
realmente con el tipo de fichero del que se trate.
A la hora de especificar ficheros a compilar también es
pueden utilizar los caracteres de comodín típicos del
sistema operativo. Por ejemplo, para compilar todos los
ficheros con extensión .cs del directorio actual en una
librería llamada Varios.dll se haría:
csc /t:library /out:varios.dll *.cs
Con lo que hay que tener cuidado, y en especial al compilar varios
fuentes, es con que no se compilen a la vez más de un tipo
de dato con punto de entrada, pues entonces el compilador no
sabría cuál usar como inicio de la aplicación.
Para orientarlo, puede especificarse como valor de la opción
/main el nombre del tipo que contenga el Main() ha
usar como punto de entrada. Así, para compilar los ficheros
A.cs y B.cs en un ejecutable cuyo punto de entrada sea el definido
en el tipo Principal, habría que escribir:
csc /main:Principal A.cs B.cs
Obviamente, para que esto funcione A.cs o B.cs tiene que contener
alguna definición de algún tipo llamado Principal con
un único método válido como punto de entrada.
(obviamente si contiene varias se volvería a tener el
problema de no saber cuál usar)
Compilación con
Visual Studio.NET
Para compilar una aplicación en Visual Studio.NET primero
hay que incluirla dentro de algún proyecto. Para ello basta
pulsar el botón New Project en la página de
inicio que se muestra nada más arrancar dicha herramienta,
tras lo que se obtendrá una pantalla con el aspecto mostrado
en la Ilustración 1.
En el recuadro de la ventana mostrada etiquetado como Project
Types se ha de seleccionar el tipo de proyecto a crear.
Obviamente, si se va a trabajar en C# la opción que
habrá que escoger en la misma será siempre Visual C#
Projects.
En el recuadro Templates se ha de seleccionar la plantilla
correspondiente al subtipo de proyecto dentro del tipo
indicado en Project Types que se va a realizar. Para
realizar un ejecutable de consola, como es nuestro caso, hay que
seleccionar el icono etiquetado como Console Application. Si se
quisiese realizar una librería habría que seleccionar
Class Library, y si se quisies realizar un ejecutable de ventanas
habría que seleccionar Windows Application. Nótese
que no se ofrece ninguna plantilla para realizar módulos, lo
que se debe a que desde Visual Studio.NET no pueden crearse.
Por último, en el recuadro de texto Name se ha
de escribir el nombre a dar al proyecto y en Location
el del directorio base asociado al mismo. Nótese que
bajo de Location aparecerá un mensaje informando
sobre cual será el directorio donde finalmente se
almacenarán los archivos del proyecto, que será el
resultante de concatenar la ruta especificada para el directorio
base y el nombre del proyecto.
Una vez configuradas todas estas opciones, al pulsar botón
OK Visual Studio creará toda la infraestructura
adecuada para empezar a trabajar cómodamente en el proyecto.
Como puede apreciarse en la Ilustración 2, esta
infraestructura consistirá en la generación de un
fuente que servirá de plantilla para la realización
de proyectos del tipo elegido (en nuestro caso, aplicaciones
de consola en C#):
A partir de esta plantilla, escribir el código de la
aplicación de ejemplo es tan sencillo con simplemente
teclear System.Console.WriteLine("¡Hola Mundo!")
dentro de la definición del método Main() creada por
Visual Studio.NET. Claro está, otra posibilidad es borrar
toda la plantilla y sustituirla por el código para HolaMundo
mostrado anteriormente.
Se haga como se haga, para compilar y ejecutar tras ello la
aplicación sólo hay que pulsar CTRL+F5 o
seleccionar Debug -> Start Without
Debugging en el menú principal de Visual Studio.NET.
Para sólo compilar el proyecto, entonces hay que seleccionar
Build -> Rebuild All. De todas formas,
en ambos casos el ejecutable generado se almacenará en el
subdirectorio Bin\Debug del directorio del proyecto.
En el extremo derecho de la ventana principal de Visual Studio.NET
puede encontrar el denominado Solution Explorer (si no lo
encuentra, seleccione View -> Solution
Explorer), que es una herramienta que permite consultar
cuáles son los archivos que forman el proyecto. Si
selecciona en él el icono correspondiente al proyecto en que
estamos trabajando y pulsa View -> Property
Pages obtendrá una hoja de propiedades del proyecto con
el aspecto mostrado en la Ilustración 3:

Esta ventana permite configurar de manera visual la mayoría
de opciones con las que se llamará al compilador en
línea de comandos. Por ejemplo, para cambiar el nombre del
fichero de salida (opción /out) se indica su nuevo
nombre en el cuadro de texto Common Properties
-> General -> Assembly
Name; para cambiar el tipo de proyecto a generar (opción
/t) se utiliza Common Properties ->
General -> Output Type (como verá
si intenta cambiarlo, no es posible generar módulos desde
Visual Studio.NET); y el tipo que contiene el punto de entrada a
utilizar (opción /main) se indica en Common
Properties -> General ->
Startup Object
Finalmente, para añadir al proyecto referencias a
ensamblados externos (opción /r) basta seleccionar
Project -> Add Reference en el
menú principal de VS.NET.