Documentos de Académico
Documentos de Profesional
Documentos de Cultura
/*
Fichero: Saludo.cs
Fecha: Enero de 2004
Autores: F. Berzal, F.J. Cortijo y J.C.Cubero
Comentarios:
Primer programa escrito en C#
*/
using System;
Console.WriteLine("¡Hola, mundo!");
Console.ReadLine(); // Enter para terminar.
}
}
Lo primero que hay que resaltar es que C# es un lenguaje sensible a las mayúsculas, por lo que,
por ejemplo, Main es diferente a main, por lo que deberá prestar atención a la hora de escribir el
código ya que la confusión entre mayúsculas y minúsculas provocará errores de compilación.
Todas las órdenes acaban con el símbolo del punto y coma (;). Los bloques de órdenes (parte
iterativa de un ciclo, partes dependientes de una instrucción condicional -parte if y parte else-,
código de un método, etc.) se encierran entre llaves {} y no se escribe el ; después de la llave de
cierre (observar en el ejemplo el método Main).
Si el lector ha programado en C++ no habrá tenido dificultad en localizar los comentarios que
hemos insertado en el programa ya que la sintaxis es idéntica en ambos lenguajes: existen
comentarios de línea, cuyo comienzo se especifica con los caracteres // y comentarios de formato
libre, delimitados por /* y */.
C# es un lenguaje orientado a objetos y todo programa debe pertenecer a una clase; en este caso
la clase se llama SaludoAlMundo.
La línea using System declara que se va a usar el espacio de nombres (namespace) llamado
System. Esta declaración no es igual a un #include de C#, tan solo evita escribir el prefijo System
cada vez que se use un elemento de ese espacio de nombres. Por ejemplo, Console es un objeto
del espacio de nombres System; en lugar de escribir su nombre completo (System.Console)
podemos escribir solamente Console al haber declarado que se va a emplear el espacio de
nombres System.
Cuando este programa se compile y se proceda a su ejecución, el primera función (estrictamente
hablando, método) en ejecutarse será Main. Los programadores de C++ deberán tener especial
cuidado y no confundirlo con main().
La función Main() tiene los siguientes prototipos válidos:
Si la función no devuelve ningún valor, puede usarse:
public static void Main( )
public static void Main(string [] args)
La diferencia está en la posibilidad de suministrar argumentos en la llamada a la
ejecución del programa. Main() procesan los argumentos tomándolos de la lista de
cadenas args (más adelante se detalla cómo).
Si la función devuelve un valor al proceso que invoca la ejecución del programa, el
valor debe ser entero (int) y, como en el caso anterior, puede usarse:
public static int Main( )
public static int Main(string [] args)
La convención es que el valor 0 indica que el programa termina correctamente.
En cuanto a las instrucciones que efectivamente se ejecutan, el método Main() llama a los métodos
WriteLine() y ReadLine() del objeto Console. El primero se encargan de mostrar una cadena en la
consola (Símbolo de Sistema, en Windows XP) y el segundo de tomar una cadena de la consola
(teclado). Aunque esta última instrucción pueda parecer innecesaria, de no escribirla se mostraría
la cadena ¡Hola, mundo! y se cerraría inmediatamente la consola, por lo que no podríamos
contemplar el resultado de la ejecución. La última instrucción detiene la ejecución del programa
hasta que se introduce una cadena (pulsar ENTER es suficiente) y a continuación termina la
ejecución del programa y se cierra la consola. Así, cuando usemos aplicaciones de consola
siempre terminaremos con esta instrucción.
A continuacón detallaremos ciertos aspectos léxicos importantes de los programas escritos en C#,
algunos de los cuales ya se han comentado brevemente como explicación al programa
introductorio.
Comentarios
Los comentarios tienen como finalidad ayudar a comprender el código fuente y están destinados,
por lo tanto, a los programadores. No tienen efecto sobre el código ejecutable ya que su contenido
es ignorado por el compilador (no se procesa). La sintaxis de los comentarios en C# es idéntica a
la de C++ y se distinguen dos tipos de comentarios:
Comentarios de línea. Están precedidos de la construcción // y su efecto (ámbito)
termina en la línea en la que está inmerso.
Comentarios de formato libre. Están delimitados por las construcciones /* y */ y
pueden extenderse por varias líneas.
Ejemplos de comentarios
/*
En múltiples líneas, como se viene
haciendo desde "los tiempos de C"
*/
123 // int
0x7B // hexadecimal
123U // unsigned
123ul // unsigned long
123L // long
Literales reales.
Los literales reales permiten escribir valores de los tipos float, double y decimal.
El tipo de un literal real se determina como sigue:
o Si no se especifica sufijo, el tipo es double.
o Si el sufijo es F o f es de tipo float.
o Si el sufijo es D o d es de tipo double.
o Si el sufijo es M o m es de tipo decimal.
Hay que tener en cuenta que, en un literal real, siempre son necesarios
dígitos decimales tras el punto decimal. Por ejemplo, 3.1F es un literal
real, pero no así 1.F.
Literales reales
1f, 1.5f, 1e10f, 123.456F, 123f y 1.23e2f // float
1d, 1.5d, 1e10d, 123.456D, 123.0 y 123D // double
1m, 1.5m, 1e10m, 123.456M, 123.456m y 12.3E1M // decimal.
Literales de caracteres.
Un literal de caracteres representa un carácter único y normalmente está
compuesto por un carácter entre comillas simples, por ejemplo 'A'.
Una secuencia de escape sencilla representa una codificación de caracteres
Unicode y está formada por el carácter \ seguido de otro carácter. Las secuencias
válidas se describe en la siguiente tabla.
Secuencia de escape Nombre del carácter Codificación Unicode
\' Comilla simple 0x0027
\" Comilla doble 0x0022
\\ Barra invertida 0x005C
\0 Null 0x0000
\a Alerta 0x0007
\b Retroceso 0x0008
\f Avance de página 0x000C
\n Nueva línea 0x000A
\r Retorno de carro 0x000D
\t Tabulación horizontal 0x0009
\v Tabulación vertical 0x000B
El tipo de un literal de caracteres es char.
Literales de caracteres
class ObjetoDemo
{
public int Valor;
}
class AppDemoRef
{
Console.ReadLine();
}
string s1 = "Hola";
string s2 = s1;
En este punto s2 referencia al mismo objeto que s1. Sin embargo, cuando el valor de s1 es
modificado, por ejemplo con:
s1 = "Adios";
lo que ocurre es que se crea un nuevo objeto string referenciado por s1. De esta forma, s1 contiene
"Adios" mientras que s2 mantiene el valor "Hola". Esto es así porque los objetos string son
immutables, por lo que, para cambiar lo que referencia una variable string debe crearse un nuevo
objeto string.
Variables y constantes
Variables
Una variable permite el almacenamiento de datos en la memoria. Es una abstracción que permite
referirnos a una zona de memoria mediante un nombre (su identificador). Todas las variables
tienen asociadas un tipo que determina los valores que pueden almacenarse y las operaciones que
pueden efectuarse con los datos de ese tipo. Además, el término variable indica que el contenido
de esa zona de memoria puede modificarse durante la ejecución del programa.
Nombres de variables
Los nombres que pueden asignarse a las variables deben regirse por unas normas básicas:
Pueden contener letras, dígitos y el caracter de subrayado (_).
No pueden empezar con un número: deben comenzar por una letra letra o con el
carácter de subrayado (_).
Finalmente, recordar que, como identificador que es, el nombre de una variable es sensible a las
mayúsculas y no pueden coincidir con una palabra reservada a no ser que tenga el prefijo @,
aunque no es una práctica recomendada.
Declaración de variables
Antes de usar una variable se debe declarar. La declaración de una variable indica al compilador
el nombre de la variable y su tipo. Una declaración permite que se pueda reservar memoria para
esa variable y restringir el espacio (cantidad de memoria) que requiere, los valores que pueden
asignarsele y las operaciones en las que puede intervenir.
La sintaxis de una declaración es sencilla: tan sólo hay que especificar el tipo de la variable y el
nombre que se le asocia. La declaración debe concluir con el carácter punto y coma. Por ejemplo,
si vamos a emplear una variable para guardar en ella el valor del área de un círculo debemos:
Darle un nombre significativo: Area
Asociarle un tipo: dado que puede tener decimales y no se requiere una gran
precisión, bastará con el tipo float.
float Area;
Cuando se van a declarar múltiples variables del mismo tipo no es necesario que cada declaración
se haga por separado, pueden agruparse en la misma línea compartiendo el tipo. Por ejemplo, las
declaraciones:
float Radio;
float Area;
pueden simplificarse en una línea:
Radio = 10.0F;
En el ejemplo se asigna el valor (literal entero) 10 a la variable Radio. El valor que tuviera
almacenado la variable Radio se pierde, quedando fijado a 10.
En la misma línea de la declaración puede asignarse un valor a la variable, por lo que declaración e
inicialización:
using System;
class Area1
{
static void Main(string[] args)
{
float Radio = 10.0F;
float Area = Area = 2 * 3.1416F * Radio * Radio;
Console.WriteLine(Area);
Console.ReadLine();
}
}
Como puede parecer evidente, emplear una variable que no ha sido declarada produce un error en
tiempo de compilación. En C#, además, hay que asignarle un valor antes de utilizarla. Si no se
hace se genera un error en tiempo de compilación ya que esta comprobación (igual que ocurre en
Java) se efectúa por el compilador.
void f()
{
int i;
Console.WriteLine(i); // Error: uso de la variable local no asignada 'i'
}
Constantes
Una constante se define de manera parecida a una variable: modeliza una zona de memoria que
puede almacenar un valor de un tipo determinado. La diferencia reside en que esa zona de
memoria (su contenido) no puede modificarse en la ejecución del programa. El valor de la
constante se asigna en la declaración.
Sintácticamente se especifica que un dato es constante al preceder con la palabra reservada const
su declaración. Por ejemplo, para declarar un a constante de tipo float llamada PI y asignarle el
valor (constante) 3.1416 se escribirá:
using System;
class Area2
{
static void Main(string[] args)
{
const float PI = 3.1416F;
float Radio = 10.0F;
float Area = 2 * PI * Radio * Radio;
Console.WriteLine(Area);
Console.ReadLine();
}
}
Ámbito de variables y constantes
El ámbito (del inglés scope) de una variable y/o constante indica en qué partes del código es lícito
su uso.
En C# el ámbito abarca desde el lugar de su declaración hasta donde termina el bloque en el que
fue declarada.
En el ámbito de una variable no puede declararse otra variable con el mismo nombre (aunque sea
en un bloque interno, algo que si está permitido en C++):
a += 22;
equivale a
a = a + 22;
o sea, primero se calcula la expresión a+22 y posteriormente,ese valor se
almacena en a.
De la misma manera actúan los demás operadores de asignación: -=, *=, /=, %=,
&=, |=, ^=, <<= y >>=.
Para asignar instancias de clases (en el sentido clásico de C++) se debe redefinir
el método MemberwiseCopy() que es heredado por todas las clases desde
System.Object (todas las clases heredan, en última instancia de System.Object).
El operador de acceso a miembros es el operador punto (.) y se emplea para
acceder a los miembros (componentes) de una clase, estructura o espacio de
nombres.
El operador de acceso por índices es el tradicional, formado por la pareja de
caracteres [ y ]. En C# también se emplea para especificar atributos.
El operador de conversión explícita de tipos (casting) es el clásico, formado por
la pareja de caracteres ( y ).
El operador ternario condicional evalúa una condición lógica y devuelve uno de
dos valores.
Se utiliza en expresiones de la forma:
using System;
class Clase1 {}
class Clase2 {}
using System;
class Clase1 {}
class Clase2 {}
if (o is Clase1)
{
Console.WriteLine ("o es de Clase1");
a = (Clase1)o;
// trabajar con a
}
else if (o is Clase2)
{
Console.WriteLine ("o es de Clase2");
b = (Clase2)o;
// trabajar con b
}
else
{
Console.WriteLine ("o no es ni de Clase1 ni de Clase2.");
}
}
public static void Main()
{
Clase1 c1 = new Clase1();
Clase2 c2 = new Clase2();
Prueba (c1);
Prueba (c2);
Prueba ("Un string");
Console.ReadLine();
}
}
El resultado es:
o es de Clase1
o es de Clase2
o no es ni de Clase1 ni de Clase2.
El operador typeof se utiliza para obtener el objeto System.Type para un tipo. Una
expresión typeof se presenta de la siguiente forma:
typeof (tipo)
donde tipo es el tipo cuyo objeto System.Type se desea obtener.
using System;
class Clase1 {}
class Clase2 {}
Type t;
if (o is Clase1)
{
a = (Clase1)o;
Console.WriteLine ("--> {0}", typeof(Clase1).FullName);
t = typeof(Clase1);
}
else
{
b = o as Clase2;
t = typeof(Clase2);
}
Console.WriteLine (t.FullName);
}
public static void Main()
{
Clase1 c1 = new Clase1();
Clase2 c2 = new Clase2();
Prueba (c1);
Prueba (c2);
Console.ReadLine();
}
}
El resultado es:
--> Clase1
Clase1
Clase2
Control de excepciones de desbordamiento: checked y unchecked
C# proporciona la posibilidad de realizar operaciones de moldeado (casting) y
aritméticas en un contexto verificado. En otras palabras, el entorno de ejecución
.NET detecta cualquier situación de desbordamiento y lanza una excepción
(OverFlowException) si ésta se manifiesta.
En un contexto verificado (checked), si una expresión produce un valor fuera del
rango del tipo de destino, el resultado depende de si la expresión es constante o
no. Las expresiones constantes producen errores de compilación, mientras que las
expresiones no constantes se evalúan en tiempo de ejecución y producen
excepciones.
Si no se especifican ni checked ni unchecked, las expresiones constantes utilizan
la verificación de desbordamiento predeterminada en tiempo de compilación, que
es checked. De lo contrario, si la expresión no es constante, la verificación de
desbordamiento en tiempo de ejecución depende de otros factores tales como las
opciones del compilador y la configuración del entorno. En el siguiente ejemplo, el
valor por defecto es unchecked:
using System;
class PruebaDesbordamiento
{
static short x = 32767; // Maximo valor short
static short y = 32767;
Post-incremento: x++
Post-decremento: x--
Valor positivo: +
Valor negative: -
No: !
Post-decremento: --x
Multiplicación: *
División: /
Multiplicativos
Resto: %
Suma: +
Aditivos
Resta: -
Mayor: >
Igualdad ==
Desigualdad !=
Bitwise AND &
Bitwise XOR ^
Bitwise OR |
Logical AND &&
Logical OR ||
Condicional ternario ?:
Asignación =, *=, /=, %=, +=, -=, <<=, >>=, &=, ^=, |=
Asociatividad
Como siempre, es mejor utilizar paréntesis para controlar el orden de evaluación
x = y = z se evalúa como x = (y = z)
x + y + z se evalúa como (x + y) + z
Estructuras de control
Las estructuras de control de C# son similares a las de C y C++. La diferencia más notable radica
en que la instrucción condicional if y los ciclos while y do están controlados por una expresión
lógica (tipo Boolean).
Esta restricción hace que las instrucciones sean más seguras al evitar posibles fuentes de error, o
al menos, facilitan la legibilidad del código. Por ejemplo, en la siguiente instrucción (válida en C++):
if (a)
la expresión a podría ser una expresión boolean pero también de tipo int, char, float *... y la
condición se evalúa como true cuando a es distinto de cero (valor entero 0, carácter 0 ó puntero
nulo, en los ejemplos). En C# se clarifica esta ambigüedad y sólo se admiten expresiones lógicas.
De esta manera, la instrucción anterior será válida sólo cuando a sea una expresión boolean.
A modo de resumen, las características propias de las estructuras de control de C# son:
goto no puede saltar dentro de un bloque (da igual, de todas formas no lo
usaremos NUNCA).
switch funciona como en Pascal (no como en C).
Se añade una nueva estructura de control iterativa: foreach.
Estructuras condicionales
if, if-else
La estructura condicional tiene la sintaxis clásica, con la diferencia indicada anteriormente acerca
del tipo de la expresión. Si debe ejecutar más de una instrucción, se encierran en un bloque,
delimitado por las llaves { y }.
Si sólo se actúa cuando la condición es cierta:
if (a > 0) if (a > 0) {
Console.WriteLine ("Positivo"); Console.WriteLine ("Positivo");
ContPositivos++;
}
Si se actúa cuando la condición es falsa se emplea la palabra reservada else:
if (a > 0) if (a > 0) {
Console.WriteLine ("Positivo"); Console.WriteLine ("Positivo");
else ContPositivos++;
Console.WriteLine ("No Positivo"); }
else {
Console.WriteLine ("No Positivo");
ContNoPositivos++;
}
Puede escribirse una instrucción condicional dentro de otra instrucción condicional, lógicamente:
if (a > 0) {
Console.WriteLine ("Positivo");
ContPositivos++;
}
else {
if (a < 0) {
Console.WriteLine ("Negativo");
ContNegativos++;
}
else {
Console.WriteLine ("Cero");
ContCeros++;
}
}
La concordancia entre if y else se establece de manera sencilla: cada else se asocia al último if que
no tenga asociado un bloque else.
switch
La estructura de selección múltiple switch funciona sobre cualquier tipo predefinido (incluyendo
string) o enumerado (enum) y debe indicar explícitamente cómo terminar cada caso (generalmente,
con break en situaciones "normales" ó throw en situaciones "anormales", aunque es posible -pero
no recomendable- emplear goto case ó return ):
using System;
class HolaMundoSwitch
{
public static void Main(String[] args)
{
if (args.Length > 0)
switch(args[0])
{
case "José":
Console.WriteLine("Hola José. Buenos días");
break;
case "Paco":
Console.WriteLine("Hola Paco. Me alegro de verte");
break;
default: Console.WriteLine("Hola {0}", args[0]);
break;
}
else
Console.WriteLine("Hola Mundo");
}
}
Especificar los parámetros al programa en Visual Studio: Utilizar el explorador de soluciones para
configurar las propiedades del proyecto.
Un ejemplo que usa un datos string para controlar la selección:
using System;
namespace ConsoleApplication14
{
class Class1
{
static int Test(string label)
{
int res;
switch(label)
{
case null:
goto case "A"; // idem case "B" o case "A"
case "B":
case "C":
res = 1;
break;
case "A":
res = 2;
break;
default:
res = 0;
break;
}
return res;
}
int i = 0;
while (i < 5) {
...
i++;
}
do...while
int i = 0;
do {
...
i++;
} while (i < 5);
for
int i;
using System;
namespace DemoGoto
{
class MainClass
{
static void Busca(int val, int[,] vector, out int fil, out int col)
{
int i, j;
for (i = 0; i < vector.GetLength(0); i++)
for (j = 0; j < vector.GetLength(1); j++)
if (vector[i, j] == val) goto OK;
throw new InvalidOperationException("Valor no encontrado");
OK:
fil = i; col = j;
}
break
Lo usaremos únicamente en sentencias switch.
continue
Mejor no lo usamos.
return
Procuraremos emplearlo sólo al final de un método para facilitar la legibilidad del código.
E/S básica
Las operaciones de entrada y salida tienen como objetivo permitir que el usuario pueda introducir
información al programa (operaciones de entrada) y que pueda obtener información de éste
(operaciones de salida). En definitiva, tratan de la comunicación entre el usuario y el programa.
La manera más simple de comunicación es mediante la consola. La consola ha sido el modo
tradicional de comunicación entre los programas y el usuario por su simplicidad. Las aplicaciones
basadas en ventanas resultan mucho más atractivas y cómodas para el usuario y es éste, sin
duda, el tipo de comunicación que deberemos emplear para productos finales. Los programas que
no requieran una mucha interacción con el usuario, no obstante, se construyen y se ponen en
explotación mucho más rápidamente si se utiliza la consola como medio de comunicación.
Aplicaciones en modo consola
Estas aplicaciones emplean la consola para representar las secuencias de entrada, salida (y error)
estándar.
Una aplicación de consola se crea en Visual Studio .NET seleccionando Archivo, Nuevo y
Proyecto. Cuando aparece la ventana Nuevo proyecto se selecciona Proyectos de Visual C# y
Aplicación de consola:
using System;
class PideNombre
{
static void Main(string[] args)
{
Console.Write ("Introduzca su nombre: "); // 1
string nombre = Console.ReadLine(); // 2
Aplicaciones Windows
Una aplicación basada en ventanas (aplicación Windows, en lo que sigue) utilizan ventanas y
componentes específicos para interactuar con el usuario. Las peticiones de datos se realizan con
componentes de entrada de texto (por ejemplo, con un TextBox) o mediante la selección en una
lista de posibilidades (por ejemplo, con un ComboBox). Las salidas pueden realizarse de múltiples
maneras, empleando componentes Label, ventanas de mensajes MessageBox, etc.
Por ejemplo, en la figura siguiente mostramos una aplicación que responde mostrando una ventana
de mensaje (MessageBox) cuando se pincha sobre el botón titulado Saludo. Basta con ejecutar
este código cada vez que se pinche en dicho botón:
MessageBox.Show ("¡Hola, mundo!", "Un saludo típico");
(en realidad, System.Windows.Forms.MessageBox.Show (...);)
Una aplicación más compleja podría pedir el nombre del usuario en un componente TextBox y
mostrarlo empleando un componente Label cuando se pincha en el botón titulado Saludo:
Una aplicación de ventanas se crea fácilmente en Visual Studio .NET seleccionando Archivo,
Nuevo y Proyecto. En la ventana Nuevo proyecto se selecciona ahora Proyectos de Visual C# y
Aplicación para Windows.
Tipos de datos
Tipos básicos
El sistema unificado de tipos. El tipo Object
Cadenas de caracteres
Vectores y matrices
Estructuras
Enumeraciones
Cuando definimos un objeto debemos especificar su tipo. El tipo determina qué valores puede
almacenar ese objeto (clase y rango) y las operaciones que pueden efectuarse con él.
Como cualquier lenguaje de programación, C# proporciona una serie de tipos predefinidos (int,
byte, char, string, ...) y mecanismos para que el usuario cree sus propios tipos (class y struct).
La estructura de tipos de C# es una gran novedad ya que establece una relación jerárquica entre
éstos, de manera que todos los tipos son clases y se construyen por herencia de la clase base
Objet. Esta particularidad hace que la creación y gestión de tipos de datos en C# sea una de las
principales novedades y potencialidades del lenguaje.
Tipos básicos
Los tipos de datos básicos son los tipos de datos más comúnmente utilizados en programación.
Los tipos predefinidos en C# están definidos en el espacio de nombres System, que es el espacio
de nombres más numeroso (e importante) de la plataforma .NET. Por ejemplo, para representar
números enteros de 32 bits con signo se utiliza el tipo de dato System.Int32 y a la hora de crear un
objeto a de este tipo que represente el valor 2 se usa la siguiente sintaxis:
System.Int32 a = 2;
Al ser un tipo valor no se utiliza el operador new para crear objetos System.Int32, sino que
directamente se indica el literal que representa el valor a crear, con lo que la sintaxis necesaria
para crear entero de este tipo se reduce considerablemente. Es más, dado lo frecuente que es el
uso de este tipo también se ha predefinido en C# el alias int para el mismo, por lo que la definición
de variable anterior queda así de compacta:
int a = 2;
System.Int32 no es el único tipo de dato básico incluido en C#. En el espacio de nombres System
se han incluido los siguientes tipos:
C# Tipo en System Características Símbolo
sbyte System.Sbyte entero, 1 byte con signo
byte System.Byte entero, 1 byte sin signo
short System.Short entero, 2 bytes con signo
ushort System.UShort entero, 2 bytes sin signo
int System.Int32 entero, 4 bytes con signo
uint System.UInt32 entero, 4 bytes sin signo U
long System.Int64 entero, 8 bytes con signo L
ulong System.ULong64 entero, 8 bytes sin signo UL
float System.Single real, IEEE 754, 32 bits F
double System.Double real, IEEE 754, 64 bits D
decimal System.Decimal real, 128 bits (28 dígitos significativos) M
bool System.Boolean (Verdad/Falso) 1 byte
char System.Char Carácter Unicode, 2 bytes ´´
string System.String Cadenas de caracteres Unicode ""
Cualquier objeto (ningún tipo
object System.Object
concreto)
Los tipos están definidos de manera muy precisa y no son dependientes del compilador o de la
plataforma en la que se usan.
La tabla anterior incorpora la columna Símbolo para indicar cómo debe interpretarse un literal. Por
ejemplo, 28UL debe interpretarse como un entero largo sin signo (ulong).
Todos los tipos enumerados son tipos valor, excepto string (que no debe confundirse con un vector
de caracteres) y Object que son tipos referencia. Recuerde, no obstante, que los datos string se
comportaban como un tipo valor ante la asgnación.
El sistema unificado de tipos. El tipo Object
En C# desaparecen las variables y funciones globales: todo el código y todos los datos de una
aplicación forman parte de objetos que encapsulan datos y código (como ejemplo, recuerde
cómo Main() es un método de una clase). Como en otros lenguajes orientados a objetos, en C# un
tipo puede incluir datos y métodos. De hecho, hasta los tipos básicos predefinidos incluyen
métodos, que, como veremos, heredan de la clase base object, a partir de la cual se construyen
implícita o explícitamente todos los tipos de datos. Por ejemplo:
int i = 10;
string c = i.ToString();
e incluso:
string c = 10.ToString();
Esta manera por la que podemos manipular los datos básicos refleja la íntima relación entre C# y la
biblioteca de clase de .NET. De hecho, C# compila sus tipos básicos asociándolos a sus
correspondientes en .NET; por ejemplo, hace corresponder al tipo string) con la clase
System.String, al tipo int) con la clase System.Int32, etc. Así, se confirma que todo es un objeto en
C# (por si aún había alguna duda).
Aunque no comentaremos todos los métodos disponibles para los tipos básicos, destacaremos
algunos de ellos.
Todos los tipos tienen un método ToString() que devuelve una cadena (string) que
representa su valor textual.
char tiene propiedades acerca de su contenido (IsLetter, IsNumber, etc.) además
de métodos para realizar conversiones (ToUpper(), ToLower(), etc.)
El tipo básico string dispone, como puede suponerse, de muchos métodos
específicos, aún más que la clase string de la biblioteca estándar de C++.
Algunos métodos estáticos y propiedades particularmente interesantes son:
Los tipos numéricos enteros tipos tienen las propiedades MinValue y MaxValue
(p.e. int.MaxValue).
Los tipos float y double tienen la propiedad Epsilon, que indica el mínimo valor
positivo que puede representarse en un dato de su tipo (p.e. float.Epsilon).
Los tipos float y double tienen definidos los valores NaN (no es un número, no está
definido), PositiveInfinite y NegativeInfinity, valores que son pueden ser devueltos
al realizar ciertos cálculos.
Muchos tipos, incluyendo todos los tipos numéricos, proporcionan el método
Parse() que permite la conversión desde un dato string (p.e. double d =
double.Parse("20.5"))
Conversiones de tipos
Una conversión de tipo (casting) puede ser implícita o explícita.
Implícitas Explícitas
Ocurren automáticamente Requieren un casting
Siempre tienen éxito Pueden fallar
No se pierde información Se puede perder información
int x = 123456;
long y = x; // implicita
short z = (short) x; // explicita (riesgo)
float f1 = 40.0F;
long l1 = (long) f1; // explicita (riesgo por redondeo)
short s1 = (short) l1; // explicita (riesgo por desbordamiento)
int i1 = s1; // implicita, no hay riesgo
uint i2 = (uint) i1; // explicita (riesgo de error por signo)
En C# las conversiones de tipo (tanto implícitas como explícitas) en las que intervengan tipos
definidos por el usuario pueden definirse y particularizarse.
Recordemos que pueden emplearse los operadores checked y unchecked para realizar
conversiones (y operaciones aritméticas) en un contexto verificado: el entorno de ejecución .NET
detecta cualquier situación de desbordamiento y lanza una excepción OverFlowException si ésta
se manifiesta.
El tipo object
Por el sistema unificado de tipos de C#, todo es un objeto. C# tiene predefinido un tipo referencia
llamado object y cualquier tipo (valor o referencia, predefinido o definido por el usuario) es en
última instancia, de tipo object (con otras palabras puede decirse que hereda todas las
características de ese tipo).
El tipo object se basa en System.Object de .NET Framework. Las variables de tipo object pueden
recibir valores de cualquier tipo. Todos los tipos de datos, predefinidos y definidos por el usuario,
heredan de la clase System.Object. La clase Object es la superclase fundamental de todas las
clases de .NET Framework; la raíz de la jerarquía de tipos.
Normalmente, los lenguajes no precisan una clase para declarar la herencia de Object porque está
implícita.
Por ejemplo, dada la siguiente declaración:
object o;
todas estas instrucciones son válidas:
o = 10; // int
o = "Una cadena para el objeto"; // cadena
o = 3.1415926; // double
o = new int [24]; // vector de int
o = false; // boolean
Dado que todas las clases de .NET Framework se derivan de Object, todos los métodos definidos
en la clase Object están disponibles en todos los objetos del sistema. Las clases derivadas pueden
reemplazar, y de hecho reemplazan, algunos de estos métodos, entre los que se incluyen los
siguientes:
public void Object() Inicializa una nueva instancia de la clase Object. Los
constructores llaman a este constructor en clases derivadas, pero éste también
puede utilizarse para crear una instancia de la clase Object directamente.
public bool Equals(object obj) Determina si el objeto especificado es igual al objeto
actual.
protected void Finalize() Realiza operaciones de limpieza antes de que un objeto
sea reclamado automáticamente por el recolector de elementos no utilizados.
public int GetHashCode() Sirve como función hash para un tipo concreto,
apropiado para su utilización en algoritmos de hash y estructuras de datos como
las tablas hash.
public string ToString() Devuelve un objeto string que representa al objeto actual.
Se emplea para crear una cadena de texto legible para el usuario que describe una
instancia de la clase. La implementación predeterminada devuelve el nombre
completo del tipo del objeto Object.
En el último ejemplo, si cada vez que asignamos un valor a o ejecutamos
Console.WriteLine (o.ToString()) o simplemente Console.WriteLine (o);
obtendremos este resultado:
10
Una cadena para el objeto
3,1415926
System.Int32[]
False
public Type GetType() Obtiene el objeto Type que representa el tipo exacto en
tiempo de ejecución de la instancia actual.
En el último ejemplo, si cada vez que asignamos un valor a o ejecutamos
Console.WriteLine (o.getType()) obtendremos este resultado:
System.Int32
System.String
System.Double
System.Int32[]
System.Boolean
protected object MemberwiseClone() Crea una copia superficial del objeto Object
actual. No se puede reemplazar este método; una clase derivada debe
implementar la interfaz ICloneable si una copia superficial no es apropiada.
MemberwiseClone() está protegido y, por tanto, sólo es accesible a través de esta
clase o de una clase derivada. Una copia superficial crea una nueva instancia del
mismo tipo que el objeto original y, después, copia los campos no estáticos del
objeto original. Si el campo es un tipo de valor, se realiza una copia bit a bit del
campo. Si el campo es un tipo de referencia, la referencia se copia, pero no se
copia el objeto al que se hace referencia; por lo tanto, la referencia del objeto
original y la referencia del punto del duplicado apuntan al mismo objeto. Por el
contrario, una copia profunda de un objeto duplica todo aquello a lo que hacen
referencia los campos del objeto directa o indirectamente.
Polimorfismo -boxing y unboxing-
Boxing (y su operación inversa, unboxing) permiten tratar a los tipos valor como objetos (tipo
referencia). Los tipos de valor, incluidos los struct y los predefinidos, como int, se pueden convertir
al tipo object (boxing) y desde el tipo object (unboxing).
La posibilidad de realizar boxing permite construir funciones polimórficas: pueden realizar una
operación sobre un objeto sin conocer su tipo concreto.
void Polim(object o)
{
Console.WriteLine(o.ToString());
}
...
Polim(42);
Polim("abcd");
Polim(12.345678901234M);
Polim(new Point(23,45));
Boxing
Boxing es una conversión implícita de un tipo valor al tipo object. Cuando se realiza boxing de un
valor, se asigna una instancia de objeto y se copia el valor en el nuevo objeto.
Por ejemplo, considere la siguiente declaración de una variable de tipo de valor:
int i = 123;
La siguiente instrucción aplica implícitamente la operación de boxing sobre la variable i:
object o = i;
El resultado de esta instrucción es crear un objeto o en la pila que hace referencia a un valor del
tipo int alojado en el heap. Este valor es una copia del valor del tipo de valor asignado a la variable
i. La diferencia entre las dos variables, i y o se muestra en la siguiente figura:
t.Add(0, "zero");
t.Add(1, "one");
t.Add(2, "two");
Una matriz dentada no es más que una tabla cuyos elementos son a su vez tablas, pudiéndose
así anidar cualquier número de tablas. Cada tabla puede tener un número propio de casillas. En el
siguiente ejemplo se pide el número de filas, y para cada fila, el número de casillas de ésa.
// Mostrar resultado
Console.WriteLine ("Contenido de la matriz: ");
for (int f=0; f<TablaDentada.Length; f++)
{
for (int c=0; c<TablaDentada[f].Length; c++)
Console.Write (TablaDentada[f][c] + " ");
Console.WriteLine();
}
Console.ReadLine();
El resultado es:
Introduzca las dimensiones:
Num. Filas: 4
Num. Cols. de fila 0: 2
Num. Cols. de fila 1: 5
Num. Cols. de fila 2: 3
Num. Cols. de fila 3: 7
Contenido de la matriz:
11 12
21 22 23 24 25
31 32 33
41 42 43 44 45 46 47
Estructuras
Una estructura (struct) se emplea para definir nuevos tipos de datos. Los nuevos tipos así
definidos son tipos valor (se almacenan en la pila).
La sintaxis para la definición de estructuras es similar a la empleada para las clases (se emplea la
palabra reservada struct en lugar de class). No obstante,
La herencia y aspectos relacionados (p.e. métodos virtuales, métodos abstractos)
no se admiten en los struct.
No se puede especificar (explícitamente) un constructor sin parámetros. Los datos
struct tienen un constructor sin parámetros predefinido y no puede reemplazarse,
sólo se permiten constructores con parámetros. En este sentido son diferentes a
los struct en C++.
En el caso de especificar algún constructor con parámetros deberíamos
asegurarnos de iniciar todos los campos.
En el siguiente ejemplo se proporciona un único constructor con parámetros:
p2 = p; // p2.x==102, p2.y==5
p2.x += 100; // p2.x==202, p2.y==5
p3.y = p.y + p2.y; // p3.y==10
p = [102,5]
p2 = [202,5]
p3 = [102,10]
Enumeraciones
Una enumeración o tipo enumerado es un tipo especial de estructura en la que los literales de
los valores que pueden tomar sus objetos se indican explícitamente al definirla.
La sintaxis es muy parecida a la empleada en C++:
enum State { Off, On };
aunque el punto y coma final es opcional ya que una enumeración es una definición de un struct y
ésta no requiere el punto y coma:
enum State { Off, On }
Al igual que en C++ y en C, C# numera a los elementos de la enumeración con valores enteros
sucesivos, asignando el valor 0 al primero de la enumeración, a menos que se especifique
explícitamente otra asignación:
enum Tamanio {
Pequeño = 1,
Mediano = 3,
Grande = 5
Inmenso
}
En el ejemplo, el valor asociado a Inmenso es 6.
Para acceder a los elementos de una enumeración debe cualificarse completamente:
Tamanio Ancho = Tamanio.Grande;
lo que refleja que cada enumeración es, en última instancia, un struct.
Si no se declara ningún tipo subyacente de forma explícita, se utiliza Int32. En el siguiente ejemplo
se especifica el tipo base byte:
enum Color: byte {
Red = 1,
Green = 2,
Blue = 4,
Black = 0,
White = Red | Green | Blue
}
La clase Enum (System.Enum) proporciona la clase base para las enumeraciones. Proporciona
métodos que permiten comparar instancias de esta clase, convertir el valor de una instancia en su
representación de cadena, convertir la representación de cadena de un número en una instancia
de esta clase y crear una instancia de una enumeración y valor especificados. Por ejemplo:
Color c1 = Color.Black;
Console.WriteLine((int) c1); // 0
Console.WriteLine(c1); // Black
Console.WriteLine(c1.ToString()); // Black
Pueden convertirse explícitamente en su valor entero (como en C). Admiten determinados
operadores aritméticos (+,-,++,--) y lógicos a nivel de bits (&,|,^,~). ejemplo:
Color c2 = Color.White;
Console.WriteLine((int) c2); // 7
Console.WriteLine(c2.ToString()); // White
Otro ejemplo más complejo:
enum ModArchivo
{
Lectura = 1,
Escritura = 2,
Oculto = 4,
Sistema = 8
}
...
ModArchivo st = ModArchivo.Lectura | ModArchivo.Escritura;
...
Console.WriteLine (st.ToString("F")); // Lectura, Escritura
Console.WriteLine (Enum.Format(typeof(ModArchivo), st, "G")); // 3
Console.WriteLine (Enum.Format(typeof(ModArchivo), st, "X")); // 00000003
Console.WriteLine (Enum.Format(typeof(ModArchivo), st, "D")); // 3
Clases (1)
Introducción
Modificadores de acceso
Variables de instancia y miembros estáticos
Campos, constantes, campos de sólo lectura y propiedades
Métodos
Constructores y "destructores"
Sobrecarga de operadores
Conversiones de tipo
Clases y structs
Introducción
Un objeto es un agregado de datos y de métodos que permiten manipular dichos datos, y un
programa en C# no es más que un conjunto de objetos que interaccionan unos con otros a
través de sus métodos.
Datos
o campos
o constantes y campos de sólo lectura
o propiedades
Métodos
o métodos generales
o métodos constructores
o métodos de sobrecarga de operadores e indexadores
Un campo es un dato común a todos los objetos de una clase. La declaración de un dato se
realiza, sintácticamente, como cualquier variable.
La palabra reservada this es una variable predefinida disponible dentro de las funciones no
estáticas de un tipo que se emplea, en un método de una clase, para acceder a los miembros
del objeto sobre el que se está ejecutando el método en cuestión. En definitiva, permite
acceder al objeto "activo". El siguiente ejemplo muestra de manera clara su aplicación:
// Método
public void MuestraCoche ()
{
Console.WriteLine (this.Marca + " " +
this.Modelo +
" (" + this.VelocMax + " Km/h)");
}
} // class CocheSimple
recordemos que el operador new se emplea para crear objetos de una clase especificada.
Cuando se ejecuta se llama a un método especial llamado constructor y devuelve una
referencia al objeto creado. Si no se especifica ningún constructor C# considera que existe
un constructor por defecto sin parámetros. Una buena costumbre es proporcionar siempre
algún constructor.
class CocheSimpleApp
{
static void Main(string[] args)
{
// "MiCoche" y "TuCoche" son variables de tipo
"CocheSimple"
// que se inicializan llamando al constructor.
CocheSimple MiCoche = new CocheSimple
("Citröen", "Xsara", 220);
CocheSimple TuCoche = new CocheSimple ("Opel",
"Corsa", 190);
Console.ReadLine ();
} // Main
} // class CocheSimpleApp
Modificadores de acceso
Los modificadores de acceso nos permiten especificar quién puede usar un tipo o un
miembro del tipo, de forma que nos permiten gestionar la encapsulación de los objetos en
nuestras aplicaciones:
using System;
class Mensaje {
public static string Bienvenida = "¡Hola!, ¿Cómo
está?";
public static string Despedida = "¡Adios!, ¡Vuelva
pronto!";
}
class MiembrosStaticApp
{
static void Main()
{
Console.WriteLine(Mensaje.Bienvenida);
Console.WriteLine(" Bla, bla, bla ... ");
Console.WriteLine(Mensaje.Despedida);
Console.ReadLine();
}
}
De cualquier forma, no conviene abusar de los miembros estáticos, ya que son básicamente
datos y funciones globales en entornos orientados a objetos.
Un campo es una variable que almacena datos, bien en una una clase, bien en una
estructura.
Constantes (const)
Una constante es un dato cuyo valor se evalúa en tiempo de compilación y, por tanto, es
implícitamente estático (p.ej. Math.PI).
public class MiClase
{
public const string version = "1.0.0";
public const string s1 = "abc" + "def";
public const int i3 = 1 + 2;
public const double PI_I3 = i3 * Math.PI;
public const double s = Math.Sin(Math.PI); //ERROR
...
}
public MiClase(string s)
{
s1 = s;
}
}
......
MiClase mio = new MiClase ("Prueba");
Console.WriteLine(mio.s1);
Console.WriteLine(MiClase.d1);
......
Prueba
1,22460635382238E-16
Propiedades
Console.WriteLine ();
Console.Write ("Mi coche: ");
MiCoche.MuestraCoche(); // LLamada al método "MuestraCoche()"
el resultado obtenido es:
Mi coche: Citröen Xsara (220 Km/h)
El tuyo: Opel Corsa (190 Km/h)
using System;
class Coche
{
// Campos
protected double velocidad=0;
public string Marca;
public string Modelo;
public string Color;
public string NumBastidor;
// Método constructor
public Coche(string marca, string modelo,
string color, string numbastidor)
{
this.Marca=marca;
this.Modelo=modelo;
this.Color=color;
this.NumBastidor=numbastidor;
}
// Método
public void Acelerar(double c)
{
Console.WriteLine("--> Incrementando veloc. en {0}
km/h", c);
this.velocidad += c;
}
// Método
public void Girar(double c)
{
Console.WriteLine("--> Girando {0} grados", c);
}
// Método
public void Frenar(double c)
{
Console.WriteLine("--> Reduciendo veloc. en {0}
km/h", c);
this.velocidad -= c;
}
// Método
public void Aparcar()
{
Console.WriteLine("-->Aparcando coche");
this.velocidad = 0;
}
} // class Coche
class EjemploCocheApp
{
MiCoche.Acelerar(100);
Console.WriteLine("La velocidad actual es de {0}
km/h",
MiCoche.Velocidad);
MiCoche.Frenar(75);
Console.WriteLine("La velocidad actual es de {0}
km/h",
MiCoche.Velocidad);
MiCoche.Girar(45);
MiCoche.Aparcar();
Console.WriteLine("La velocidad actual es de {0}
km/h",
MiCoche.Velocidad);
Console.ReadLine();
}
} // class EjemploCocheApp
Métodos
Implementan las operaciones que se pueden realizar con los objetos de un tipo concreto.
Constructores, destructores y operadores son casos particulares de métodos. Las
propiedades y los indexadores se implementan con métodos (get y set).
int x = 10;
El siguiente código emplea una función que suma todos los parámetros que recibe. El
número de éstos es indeterminado, aunque debe asegurarse que sean de tipo int:
Constructores y "destructores"
Los constructores son métodos especiales que son invocados cuando se instancia una clase
(o un struct).
C# permite especificar código para inicializar una clase mediante un constructor estático.
El constructor estático se invoca una única vez, antes de llamar al constructor de una
instancia particular de la clase o a cualquier método estático de la clase. Sólo puede haber
un constructor estático por tipo y éste no puede tener parámetros.
Destructores: Se utilizan para liberar los recursos reservados por una instancia (justo antes
de que el recolector de basura libere la memoria que ocupe la instancia).
class Foo
{
~Foo()
{
Console.WriteLine("Destruido {0}", this);
}
}
Sobrecarga de operadores
Como en C++, se pueden sobrecargar (siempre con un método static) algunos operadores
unarios (+, -, !, ~, ++, --, true, false) y binarios (+, -, *, /, %, &, |, ^, ==, !=, <, >, <=, >=,
<, >).
class OverLoadApp
{
// Operadores de igualdad
// Operadores aritméticos
Console.WriteLine ();
Point p4 = p1 + p3;
Console.WriteLine ("p4 (p1+p3) es: ({0},{1})",
p4.X, p4.Y);
Point p5 = p1 - p1;
Console.WriteLine ("p5 (p1-p1) es: ({0},{1})",
p5.X, p5.Y);
Console.WriteLine ();
Console.WriteLine ("p1 es: ({0},{1})", p1.X,
p1.Y);
Console.WriteLine ("p2 es: ({0},{1})", p2.X,
p2.Y);
Console.WriteLine ("p3 es: ({0},{1})", p3.X,
p3.Y);
Console.WriteLine ("p4 es: ({0},{1})", p4.X,
p4.Y);
Console.WriteLine ("p5 es: ({0},{1})", p5.X,
p5.Y);
Console.ReadLine ();
}
}
Para asegurar la compatibilidad con otros lenguajes de .NET:
// Operadores aritméticos
// Operadores de igualdad
......
// Operadores relacionales
......
} // class Point
Conversiones de tipo
Pueden programarse las conversiones de tipo, tanto explícitas como implícitas):
using System;
class ConversionesApp
{
} // class Euro
Console.ReadLine ();
}
}
using System;
struct SPoint
{
private int x, y; // Campos
class CPoint
{
private int x, y; // Campos
class Class2App
{
static void Main(string[] args)
{
SPoint sp = new SPoint(2,5);
sp.X += 100;
int spx = sp.X; // spx = 102
Aunque las coincidencias son muchas, existen, no obstante, existen algunas diferencias
entre ellos:
Cuando se crea un objeto struct mediante el operador new, se crea y se llama al constructor
apropiado. A diferencia de las clases, se pueden crear instancias de las estructuras sin
utilizar el operador new. Si no se utiliza new, los campos permanecerán sin asignar y el
objeto no se podrá utilizar hasta haber inicializado todos los campos.
Clase Struct
Tipo referencia Tipo valor
Para las estructuras no existe herencia: una
estructura no puede heredar de otra estructura o
Puede heredar de
clase, ni puede ser la base de una clase. Sin
otro tipo (que no
embargo, las estructuras heredan de la clase base
esté "sellado")
Object. Una estructura puede implementar
interfaces del mismo modo que las clases.
Puede tener un
constructor sin No puede tener un constructor sin parámetros
parámetros
No pueden Se pueden crear instancias de las estructuras sin
crearse instancias utilizar el operador new, pero los campos
sin emplear el permanecerán sin asignar y el objeto no se podrá
operador new. utilizar hasta haber iniciado todos los campos.
Puede tener un
No puede tener destructor
destructor
Supongamos la clase MiClase y el struct de tipo MiStruct. Según sabemos de C#, las
instancias de MiClase se almacenan en el heap mientras las instancias de MiStruct se
almacenan en la pila:
MiClase cl;
Declara una referencia (igual a la declaración de
un puntero no inicializado en C++).
Crea una instancia de MiClase. LLama al
cl = new
MiClase(); constructor sin parámetros de la clase. Además,
reserva memoria en el heap.
Crea una instancia de MiStruct pero no llama a
MiStruct st; ningún constructor. Los campos de st quedan sin
iniciar.
Llama al constructor: se inicializan los campos
st = new
MiStruct(); con los valores por defecto. No se reserva
memoria porque st ya existe en la pila.
Herencia
Concepto de herencia
Clases abstractas
Clases selladas
Tipos anidados
Operadores especiales
Concepto de herencia
El mecanismo de herencia es uno de los pilares fundamentales en los que se basa la
programación orientada a objetos. Es un mecanismo que permite definir nuevas clases a
partir de otras ya definidas. Si en la definición de una clase indicamos que ésta deriva de
otra, entonces la primera -a la que se le suele llamar clase hija o clase derivada- será
tratada por el compilador automáticamente como si su definición incluyese la definición de
la segunda -a la que se le suele llamar clase padre o clase base.
A los miembros definidos en la clase hija se le añadirán los que hubiésemos definido en la
clase padre: la clase derivada "hereda" de la clase base.
La palabra clave base se utiliza para obtener acceso a los miembros de la clase base desde
una clase derivada.
Herencia de constructores
Los objetos de una clase derivada contarán con los mismos miembros que los objetos de la
clase base y además incorporarán nuevos campos y/o métodos. El constructor de una clase
derivada puede emplear el constructor de la clase base para inicializar los campos
heredados de la clase padre con la construcción base. En realidad se trata de una llamada al
constructor de la clase base con los parámetros adecuados.
: base(<parametrosBase>)
public class B
{
private int h; // Campo
} // class D
......
B varB1 = new B(); // Const. sin parámetros de B
B varB2 = new B(5); // Const. con 1 parámetro de B
Console.WriteLine("varB1 : (H={0})", varB1.H);
Console.WriteLine("varB2 : (H={0})\n", varB2.H);
using System;
namespace DemoHerencia {
class CocheSimple
{
private int VelocMax;
private string Marca;
private string Modelo;
public CocheSimple () {
this.VelocMax = 0;
this.Marca = "??";
this.Modelo = "??";
}
public CocheSimple (string marca, string mod, int
velMax)
{
this.VelocMax = velMax;
this.Marca = marca;
this.Modelo = mod;
}
} // class CocheSimple
public Taxi () {}
public Taxi (string marca, string mod, int vel,
string lic) : base (marca, mod,
vel)
{
this.CodLicencia = lic;
}
public string Licencia {
get { return this.CodLicencia; }
}
} // class Taxi
class DemoHerenciaApp {
CocheSimple MiCoche =
new CocheSimple ("Citröen", "Xsara Picasso",
220);
CocheSimple TuCoche =
new CocheSimple ("Opel", "Corsa", 190);
CocheSimple UnCoche = new CocheSimple ();
Console.WriteLine();
Console.ReadLine ();
} // Main
} // class DemoHerenciaApp
} // namespace DemoHerencia
Redefinición de métodos
Siempre que se redefine un método que aparece en la clase base, hay que utilizar
explícitamente la palabra reservada override y, de esta forma, se evitan redefiniciones
accidentales (una fuente de errores en lenguajes como Java o C++).
Sabemos que todos los objetos (incluidas las variables de los tipos predefinidos) derivan, en
última instancia, de la clase Object. Esta clase proporciona el método ToString que crea
una cadena de texto legible para el usuario que describe una instancia de la clase. Si
dejamos sin redefinir este método y empleando la clase CocheSimple las siguientes
instrucciones:
CocheSimple MiCoche =
new CocheSimple ("Citröen", "Xsara Picasso",
220);
Mi coche: DemoHerencia.CocheSimple
class CocheSimple
{
...
public override string ToString()
{
return (this.Marca + " " + this.Modelo +
" (" + this.VelocMax + " Km/h)");
}
...
}
por lo que podemos sutituir las instrucciones que muestran los datos de los objetos
CocheSimple por:
La palabra reservada base sirve para hacer referencia a los miembros de la clase base que
quedan ocultos por otros miembros de la clase actual. Por ejemplo, podríamos redefinir
también el método ToString de la clase Taxi empleando el método redefinido ToString
de la clase base CocheSencillo:
class CocheSimple
{
...
public override string ToString()
{
return (this.Marca + " " + this.Modelo +
" (" + this.VelocMax + " Km/h)");
}
...
}
class Taxi : CocheSimple
{
...
public override string ToString()
{
return (base.ToString() + "\n Licencia: " +
this.Licencia);
}
...
}
......
Taxi ElTaxiDesconocido = new Taxi ();
Console.WriteLine ("Un taxi sin identificar: " +
ElTaxiDesconocido);
y el resultado es:
En la sección dedicada a la classes.xml#Sobrecarga
sobrecarga de operadores
introdujimos la clase Point. No había ningún método que mostrara los datos de interés de
un objeto de tipo Point. Podemos sobreescribir el método ToString de manera que fuera:
Ahora las instrucciones de escritura se convierten en llamadas a este método, por ejemplo:
Un método es virtual si puede redefinirse en una clase derivada. Los métodos son no
virtuales por defecto.
void HandleShape(Shape s)
{
...
s.Draw(); // Polimorfismo
...
}
HandleShape(new Box());
HandleShape(new Sphere());
HandleShape(new Shape());
Clases abstractas
Una clase abstracta es una clase que no puede ser instanciada. Se declara empelando la
palabra reservada abstract.
Permiten incluir métodos abstractos y métodos no abstractos cuya implementación hace
que sirvan de clases base (herencia de implementación). Como es lógico, no pueden estar
"selladas".
Métodos abstractos
Un método abstracto es un método sin implementación que debe pertenecer a una clase
abstracta. Lógicamente se trata de un método virtual forzoso y su implementación se
realizará en una clase derivada.
void HandleShape(Shape s)
{
...
s.Draw();
...
}
HandleShape(new Box());
HandleShape(new Sphere());
HandleShape(new Shape()); // Error !!!
Clases selladas
Una clase sellada (sealed), es una clase de la que no pueden derivarse otras clases (esto es,
no puede utilizarse como clase base). Obviamente, no puede ser una clase abstracta.
¿Para qué sirve sellar clases? Para evitar que se puedan crear subclases y optimizar el
código (ya que las llamadas a las funciones de una clase sellada pueden resolverse en
tiempo de compilación).
Tipos anidados
C# permite declarar tipos anidados, esto es, tipos definidos en el ámbito de otro tipo. El
anidamiento nos permite que el tipo anidado pueda acceder a todos los miembros del tipo
que lo engloba (independientemente de los modificadores de acceso) y que el tipo esté
oculto de cara al exterior (salvo que queramos que sea visible, en cuyo caso habrá que
especificar el nombre del tipo que lo engloba para poder acceder a él).
Operadores especiales
is
No conviene abusar de este operador (es preferible diseñar correctamente una jerarquía de
tipos).
as
Intenta convertir de tipo una variable (al estilo de los casts dinámicos de C++). Si la
conversión de tipo no es posible, el resultado es null. Es más eficiente que el operador is,
si bien tampoco es conveniente abusar del operador as.
if (c != null) c.Drive();
}
typeof
...
Console.WriteLine(typeof(int).FullName);
Console.WriteLine(typeof(System.Int).Name);
Console.WriteLine(typeof(float).Module);
Console.WriteLine(typeof(double).IsPublic);
Console.WriteLine(typeof(Point).MemberType);
...
Clases (2)
Indexadores (indexers)
Interfaces
Delegados
Indexadores (indexers)
C# no permite, hablado con rigor, la sobrecarga del operador de acceso a tablas [ ]. Si
permite, no obstante, definir lo que llama un indexador para una clase que permite la
misma funcionalidad.
Los indexadores permiten definir código a ejecutar cada vez que se acceda a un objeto del
tipo del que son miembros usando la sintaxis propia de las tablas, ya sea para leer o
escribir. Esto es especialmente útil para hacer más clara la sintaxis de acceso a elementos
de objetos que puedan contener colecciones de elementos, pues permite tratarlos como si
fuesen tablas normales.
A diferencia de las tablas, los índices que se les pase entre corchetes no tiene porqué ser
enteros, pudiéndose definir varios indexadores en un mismo tipo siempre y cuando cada
uno tome un número o tipo de índices diferente.
Igual que las propiedades, pueden ser de sólo lectura, de sólo escritura o de
lectura y escritura.
El nombre dado a un indexador siempre ha de ser this.
Lo que diferenciará a unos indexadores de otros será el número y tipo de sus
índices.
Ejemplo de indexador
using System;
namespace IndexadorCoches
{
public class Coche
{
private int VelocMax;
private string Marca;
private string Modelo;
} // class Coche
} // class Coches
class IndexadorCochesApp
{
static void Main(string[] args)
{
// Crear una colección de coches
Coches MisCoches = new Coches (); // Por
defecto (10)
MisCoches.NumCoches = 3;
// Mostrar la colección
Console.WriteLine ();
for (int i=0; i<MisCoches.NumCoches; i++)
{
Console.Write ("Coche Num.{0}: ", i+1);
Console.WriteLine (MisCoches[i]); //
Acceso ("get")
}
Console.ReadLine ();
//
*********************************************
// Crear una colección de coches
Coches TusCoches = new Coches (4);
// Mostrar la colección
Console.WriteLine ();
for (int i=0; i<TusCoches.NumCoches; i++)
{
Console.Write ("Coche Num.{0}: ", i+1);
Console.WriteLine (TusCoches[i]); //
Acceso ("get")
}
Console.ReadLine ();
} // Main
} // class IndexadorCochesApp
} // namespace IndexadorCoches
Interfaces
Un interfaz define un contrato semántico que ha de respetar cualquier clase (o struct) que
implemente el interfaz.
Una interfaz puede verse como una forma especial de definir clases que sólo cuenten con
miembros abstractos. Sin embargo, todo tipo que derive de una interfaz ha de dar una
implementación de todos los miembros que hereda de esta, y no como ocurre con las clases
abstractas donde es posible no darla si se define como abstracta también la clase hija.
Ejemplo de polimorfismo
using System;
namespace Interface1
{
class Interface1App
{
// Definición de una interface
// Ejemplo de polimorfismo
demo = c1;
demo.MetodoDeIDemo();
demo = c2;
demo.MetodoDeIDemo();
Console.ReadLine();
}
}
}
Otro ejemplo:
using System;
class InterfaceApp
{
interface IPresentable
{
void Presentar();
}
Console.WriteLine("\nINTRO para
VerDatos(proveedor)");
Console.ReadLine();
VerDatos(p);
Console.ReadLine();
}
}
El principal uso de las interfaces es indicar que una clase implementa ciertas características.
Por ejemplo, el ciclo foreach trabaja internamente comprobando que la clase sobre la que
se aplica implementa el interfaz IEnumerable y llamando a los métodos definidos en esa
interfaz.
Herencia múltiple
using System;
namespace Interface2
{
class Interface2App
{
// Definición de interfaces
}
static void Main(string[] args)
{
Clase1 c1 = new Clase1();
Clase2 c2 = new Clase2();
Clase3 c3 = new Clase3();
IDemo1 i1;
IDemo2 i2;
IDemo3 i3;
c1.Metodo1DeInterface1();
Console.WriteLine(c1.Metodo2DeInterface1());
Console.WriteLine();
i1 = c3;
Console.WriteLine("Cuando i1 = c3 ");
i1.Metodo1DeInterface1();
Console.WriteLine(i1.Metodo2DeInterface1());
Console.WriteLine();
i3 = c3;
Console.WriteLine("Cuando i3 = c3 ");
i3.Metodo1DeInterface1();
Console.WriteLine(i3.Metodo2DeInterface1());
i3.Metodo1DeInterface3("Aplicado a i3: ");
Console.WriteLine();
i1 = c2;
Console.WriteLine("Cuando i1 = c2 ");
i1.Metodo1DeInterface1();
Console.WriteLine(i1.Metodo2DeInterface1());
i2 = c2;
Console.WriteLine("Ahora i2 = c2 ");
i2.Metodo1DeInterface2();
Console.WriteLine();
Console.ReadLine();
}
}
}
interface IControl
{
void Delete();
}
Delegados
Un delegado es un tipo especial de clase que define la signatura de un método. Su función
es similar a la de los punteros a funciones en lenguajes como C y C++ (C# no soporta
punteros a funciones).
Los delegados pueden pasarse a métodos y pueden usarse para llamar a los métodos de los
que contienen referencias.
Un "tipo" delegado
using System;
class Delegate1App
{
// Declaración del "tipo delegado" llamado
// "Del": funciones que devuelven un double
// y reciben un double.
delegate double Del (double x);
// Llamadas
Console.WriteLine (del1(0)); // 0
Console.WriteLine (del2(0)); // 1
Console.WriteLine (del3(10)); // 11
Console.ReadLine();
}
}
Para hacer más evidente el polimorfismo el método Main podría escribirse como:
Del f;
f = f1; Console.WriteLine (f(0)); // 0
f = f2; Console.WriteLine (f(0)); // 1
f = f3; Console.WriteLine (f(10)); // 11
}
o bien así:
Los delegados son muy útiles ya que permiten disponer de objetos cuyos métodos puedan
ser modificados dinámicamente durante la ejecución de un programa.
En general, son útiles en todos aquellos casos en que interese pasar métodos como
parámetros de otros métodos.
Los delegados son la base sobre la que se monta la gestión de eventos en la plataforma
.NET.
Multicasting
Un delegado es un tipo especial de clase cuyos objetos pueden almacenar referencias a uno
o más métodos, de tal manera que a través del objeto sea posible solicitar la ejecución en
cadena de todos ellos.
Un delegado puede contener e invocar múltiples métodos. De esta forma se puede hacer
multicasting de una forma sencilla y elegante. Para que un delegado pueda contener varios
métodos, éstos no pueden devolver ningún valor (si lo intentasen devolver, se generaría una
excepción en tiempo de ejecución).
"Multicasting"
using System;
class Delegate2App
{
delegate void SomeEvent (int x, int y);
Console.WriteLine("Llamada a func");
func(1,2); // Se llama tanto a Func1 como a Func2
Console.WriteLine("Llamada a func");
func(2,3); // Sólo se llama a Func2
Console.ReadLine();
}
}
Cada delegado tiene una lista ordenada de métodos que se invocan secuencialmente (en el
mismo orden en el que fueron añadidos al delegado). Los operadores += y -= se utilizan
para añadir y eliminar métodos de la lista asociada a un delegado.
Siempre se pueden utilizar interfaces en vez de delegados. Los interfaces son más
versátiles, ya que pueden encapsular varios métodos y permiten herencia, si bien los
delegados resultan más adecuados para implementar manejadores de eventos. Con los
delegados se escribe menos código y se pueden implementar fácilmente múltiples
manejadores de eventos en una única clase.
Eventos
El callback consiste en que un cliente notifica a un servidor que desea ser informado
cuando alguna acción tenga lugar. C# usa los eventos de la misma manera que Visual Basic
usa los mensajes.
Las aplicaciones en Windows se programan utilizando eventos, pues los eventos resultan
especialmente indicados para la implementación de interfaces interactivos. Cuando el
usuario hace algo (pulsar una tecla, hacer click con el ratón, seleccionar un dato de una
lista...), el programa reacciona en función de la acción del usuario.
Eventos en C#
public MyForm()
{
okButton = new Button(...);
okButton.Caption = "OK";
okButton.Click += new EventHandler(OkClicked);
}
}
Aspectos avanzados de C#
Excepciones
Reflexión
Atributos
Excepciones
Las excepciones ofrecen varias ventajas respecto a otros métodos de notificación de error,
como los códigos devueltos (órdenes return) ya que ningún error pasa desapercibido (las
excepciones no pueden ser ignoradas) y no tienen por qué tratarse en el punto en que se
producen. Los valores no válidos no se siguen propagando por el sistema. No es necesario
comprobar los códigos devueltos. Es muy sencillo agregar código de control de
excepciones para aumentar la confiabilidad del programa.
El proceso es el siguiente:
La sentencia throw lanza una excepción (una instancia de una clase derivada
de System.Exception, que contiene información sobre la excepción:
Message, StackTrace, HelpLink, InnerException...).
El bloque try delimita código que podría generar una excepción.
El bloque catch indica cómo se manejan las excepciones. Se puede relanzar
la excepción capturada o crear una nueva si fuese necesario. Se pueden
especificar distintos bloques catch para capturar distintos tipos de
excepciones. En ese caso, es recomendable poner primero los más
específicos (para asegurarnos de que capturamos la excepción concreta).
El bloque finally incluye código que siempre se ejecutará (se produzca o
no una excepción).
Ejemplo 1
El siguiente ejemplo pone de manifiesto el flujo de ejecución que sigue un programa que
lanza y procesa una excepción.
try {
Console.WriteLine("try");
throw new Exception("Mi excepcion");
}
catch {
Console.WriteLine("catch");
}
finally {
Console.WriteLine("finally");
}
try
catch
finally
Ejemplo 2
La clase Exception es la clase base de la que derivan las excepciones. La mayoría de los
objetos de excepción son instancias de alguna clase derivada de Exception, pero se puede
iniciar cualquier objeto derivado de la clase Object como excepción. En casi todos los
casos, es recomendable iniciar y detectar sólo objetos Exception.
try {
Console.WriteLine("try");
throw new Exception("Mi excepcion");
}
catch (Exception e)
{
Console.WriteLine("catch");
Console.WriteLine("Excepción detectada: " +
e.Message);
}
catch {
Console.WriteLine("catch");
}
finally {
Console.WriteLine("finally");
}
try
catch
Excepción detectada: Mi excepcion
finally
Ejemplo 3
try
{
Console.WriteLine("--> try");
denominador = Convert.ToInt16(strDen);
cociente = numerador / denominador;
Console.WriteLine ("Cociente = {0}", cociente);
}
catch (ArithmeticException e)
{
Console.WriteLine("--> catch");
Console.WriteLine("Excep. aritmética");
Console.WriteLine("ArithmeticException Handler:
{0}",
e.ToString());
}
catch (ArgumentNullException e)
{
Console.WriteLine("--> catch");
Console.WriteLine("Excep. de argumento nulo");
Console.WriteLine("ArgumentNullException
Handler: {0}",
e.ToString());
}
catch (Exception e)
{
Console.WriteLine("--> catch");
Console.WriteLine("generic Handler: {0}",
e.ToString());
}
finally
{
Console.WriteLine("--> finally");
}
Console.ReadLine();
}
}
Ejemplo 4
Hemos visto que pueden conocerse los detalles de la excepción que se haya producido.
Podemos conocer más detalles usando la propiedad StackTrace. Esta propiedad contiene
un seguimiento de pila que se puede utilizar para determinar dónde se ha producido un
error. El seguimiento de pila contiene el nombre del archivo de código fuente y el número
de la línea del programa si está disponible la información de depuración.
using System;
using System.Diagnostics;
class ExcepApp
{
static void Main(string[] args)
{
int numerador = 10;
Console.WriteLine ("Numerador es = {0}",
numerador);
try
{
Console.WriteLine("--> try");
denominador = Convert.ToInt16(strDen);
cociente = numerador / denominador;
Console.WriteLine ("Cociente = {0}", cociente);
}
catch (Exception e)
{
Console.WriteLine("--> catch");
Console.WriteLine("Generic Handler: {0}",
e.ToString());
Console.WriteLine();
Console.WriteLine("Traza de la pila:");
for (int i = 0; i < st.FrameCount; i++) {
StackFrame sf = st.GetFrame(i);
Console.WriteLine(" Pila de llamadas, Método:
{0}",
sf.GetMethod() );
Console.WriteLine(" Pila de llamadas, Línea :
{0}",
sf.GetFileLineNumber());
}
Console.WriteLine();
}
finally
{
Console.WriteLine("--> finally");
}
Console.ReadLine();
}
}
Reflexión
La capacidad de reflexión de la plataforma .NET (similar a la de la plataforma Java) nos
permite explorar información sobre los tipos de los objetos en tiempo de ejecución.
Using System;
object o = iderivada;
ClaseBase b = iderivada;
Console.WriteLine("object o = iderivada: Type is
{0}",
o.GetType());
Console.WriteLine("ibase b = iderivada: Type is
{0}",
b.GetType());
Console.ReadLine();
}
}
La reflexión puede emplearse para examinar los métodos, propiedades, ... de una clase:
Métodos de una clase
using System;
using System.Reflection;
class ReflectApp
{
public class Test
{
private int n;
// Parámetros
if (ParInf.Length > 0) {
Console.WriteLine (" Parámetros: " );
foreach (ParameterInfo p in ParInf)
Console.WriteLine(" " + p.ParameterType
+ " " + p.Name);
}
}
Console.ReadLine ();
}
}
Atributos
Un atributo es información que se puede añadir a los metadatos de un módulo de código.
Los atributos nos permiten "decorar" un elemento de nuestro código con información
adicional.
C# es un lenguaje imperativo, pero, como todos los lenguajes de esta categoría, contiene
algunos elementos declarativos. Por ejemplo, la accesibilidad de un método de una clase se
especifica mediante su declaración como public, protected, private o internal. C#
generaliza esta capacidad permitiendo a los programadores inventar nuevas formas de
información declarativa, anexarlas a distintas entidades del programa y recuperarlas en
tiempo de ejecución. Los programas especifican esta información declarativa adicional
mediante la definición y el uso de atributos.
Esta información puede ser referente tanto al propio módulo o el ensamblado al que
peretenezca, como a los tipos de datos definidos en él, sus miembros, los parámetros de sus
métodos, los bloques set y get de sus propiedades e indexadores o los bloques add y
remove de sus eventos. Se pueden emplear en ensamblados, módulos, tipos, miembros,
valores de retorno y parámetros.
Atributos predefinidos
Atributo Descripción
Browsable
Propiedades y eventos que deben mostrarse en el
inspector de objetos.
Clases y estructuras que pueden "serializarse" (esto
Serializable es, volcarse en algún dispositivo de salida, p.ej.
disco), como en Java.
Obsolete
El compilador se quejará si alguien los utiliza
(deprecated en Java).
ProgId COM Prog ID
Transaction Características transaccionales de una clase.
Observar como al marcar como obsoleta la clase A se genera un error al compilar el módulo
ya que se emplea en la línea comentada.
...
class SimpleAtrPredefApp
{
[AttributeUsage(AttributeTargets.All)]
public class HelpAttribute: Attribute
{
public string Topic = null;
private string url;
[Help("http://decsai.ugr.es/Clase1.htm")]
class Clase1
{
/* Bla, bla, bla... */
}
[Help("http://decsai.ugr.es/Clase2.htm",
Topic="Atributos")]
class Clase2
{
/* Bla, bla, bla... */
}
En este ejemplo, el atributo HelpAttribute está asociado con las clases Clase1 y Clase2.
Nota: Por convención, todos los nombres de atributo finalizan con la palabra "Attribute"
para distinguirlos de otros elementos de .NET Framework. No obstante, no tiene que
especificar el sufijo de atributo cuando utiliza atributos en el código (véase el ejemplo).
Ejemplo 1
El siguiente ejemplo muestra la manera básica de utilizar la reflexión para obtener acceso a
los atributos:
class AtributosSimpleApp
{
static void Main(string[] args)
{
MemberInfo info1 = typeof(Clase1);
object[] attributes1 =
info1.GetCustomAttributes(true);
for (int i = 0; i < attributes1.Length; i ++) {
System.Console.WriteLine(attributes1[i]);
}
MemberInfo info2 = typeof(Clase2);
object[] attributes2 =
info2.GetCustomAttributes(true);
for (int i = 0; i < attributes2.Length; i ++) {
System.Console.WriteLine(attributes2[i]);
}
Console.ReadLine();
} // Main ()
} // class AtributosSimpleApp
Ejemplo 2
Atributos y reflexión
using System;
using System.Diagnostics;
using System.Reflection;
[AttributeUsage(AttributeTargets.All)]
public class IsTestedAttribute : Attribute {
public override string ToString() { return ("
REVISADO"); }
}
[AttributeUsage(AttributeTargets.All)]
public class HelpAttribute: Attribute {
public string Topic = null;
private string url;
[AttributeUsage(AttributeTargets.Class |
AttributeTargets.Struct)]
public class CodeReviewAttribute: System.Attribute
{
public CodeReviewAttribute(string reviewer, string
date) {
this.reviewer = reviewer;
this.date = date;
this.comment = "";
}
string reviewer;
string date;
string comment;
}
[IsTested]
public override string ToString()
{
return (this.c1c1.ToString() +
this.c2c1.ToString());
}
}
[CodeReview("Juani", "12-11-2002",
Comment="Excelente")]
[Help("http://decsai.ugr.es/Clase3.htm",
Topic="Atributos")]
class Clase2
{
string c1c2;
[IsTested]
public char Met1Clase2 ()
{
return (this.c1c2[0]);
}
}
[IsTested]
public Clase3 (int n1) {
this.c1c3 = n1;
}
}
class Atributos1App
{
private static bool IsMemberTested (MemberInfo
member)
{
foreach (object attr in
member.GetCustomAttributes(true))
if (attr is IsTestedAttribute) return true;
return false;
}
member.GetCustomAttributes(typeof(HelpAttribute),
true);
if (arr.Length == 0)
Console.WriteLine("Esta clase no tiene
ayuda.");
else {
HelpAttribute ha = (HelpAttribute) arr[0];
Console.WriteLine (ha.ToString());
}
*/
foreach (object attribute in
member.GetCustomAttributes(true))
{
if (attribute is HelpAttribute)
Console.WriteLine (" Atributos de
ayuda:");
if (attribute is CodeReviewAttribute)
Console.WriteLine (" Atributos de
Revisión:");
if (attribute is IsTestedAttribute)
Console.WriteLine (" Atributos de
Actualización:");
Console.WriteLine(attribute);
}
}
t[0] = typeof(Clase1);
t[1] = typeof(Clase2);
t[2] = typeof(Clase3);
DumpAttributes(t[i]);
}
Console.ReadLine();
}
}
Programas en C#
Organización lógica de los tipos
Organización física de los tipos
Ejecución de aplicaciones
Código no seguro
Preprocesador
Documentación
Los espacios de nombres son mecanismos para controlar la visibilidad (ámbito) de los
nombres empleados e un programa. Su propósito es el de facilitar la combinación de los
componentes de un programa (que pueden provenir de varias fuentes) minimizando los
conflictos entre identificadores.
Por un lado estos espacios permiten tener más organizados los tipos de
datos, lo que facilita su localización. Así es como está organizada la BCL:
todas las clases más comúnmente usadas en cualquier aplicación pertenecen
al espacio de nombres llamado System, las de acceso a bases de datos en
System.Data, las de realización de operaciones de entrada/salida en
System.IO, etc.
Por otro lado, los espacios de nombres también permiten poder usar en un
mismo programa clases homónimas, siempre que pertenezcan a espacios de
nombres diferentes y queden perfectamente cualificadas.
namespace Graficos2D
{
public class Point { ... }
}
Los componentes del espacio de nombres no son visibles directamente desde fuera del
espacio en el que están inmersos (al igual que los componentes de un struct, de una
enumeración, de una clase, ...) a no ser que se cualifiquen completamente:
namespace Graficos2D
{
public class Point { ... }
}
class MainClass
{
Graficos2D.Point p;
La declaración
Point p;
produciría un error de compilación. En el siguiente ejemplo se trabaja con dos espacios de
nombres que incluyen una clase homónima (Point) en cada uno de ellos. Observe el uso
correcto de cada una de las dos clases cuando se cualifican completamente:
using System;
namespace Graficos2D
{
public class Point { ... }
}
namespace Graficos3D
{
public class Point { ... }
}
class MainClass
{
Graficos2D.Point p1;
Graficos3D.Point p2;
namespace N1
{
public class C1
{
public class C2 {}
}
namespace N2
{
public class C2 {}
}
}
class MainClass
{
N1.C1 o1;
N1.C1.C2 o2;
N1.N2.C2 o3;
La directiva using
using System;
namespace N1
{
public class C1
{
public class C2 {}
}
namespace N2
{
public class C2 {}
}
}
namespace DemoNamespace
{
using N1;
class MainClass
{
C1 o1; // N1 es implícito (N1.C1)
N1.C1 o1_bis; // Tipo totalmente cualificado
La directiva using también puede usarse para crear un alias para un espacio de nombres
(alias using).
Uso de alias
using System;
namespace Graficos2D
{
public class Point { }
}
namespace Graficos3D
{
public class Point { }
}
namespace DemoNamespace
{
using Point2D = Graficos2D.Point;
using Point3D = Graficos3D.Point;
class MainClass
{
Point2D p1;
Point3D p2;
}
}
}
using System;
namespace Graficos2D
{
public class Point { }
public class Almacen {}
}
namespace DemoNamespace
{
using Graficos2D;
class MainClass
{
Point p1;
int[] Almacen = new int[20];
}
}
}
Ambos son ficheros que contienen definiciones de tipos de datos. Se diferencian en que
sólo los primeros (.exe) disponen de un método especial que sirve de punto de entrada a
partir del que es posible ejecutar el código que contienen haciendo una llamada desde la
línea de órdenes del sistema operativo.
Referencias
Los espacios de nombres son una construcción del lenguaje para abreviar nombres,
mientras que las referencias son las que especifican qué assembly utilizar.
Un ejercicio
Sobre un proyecto nuevo, trabajaremos con dos ficheros de código: uno contendrá el
método Main() y el otro un espacio de nombres con una clase que se empelará en Main().
Ejecución de aplicaciones
Gestión de memoria
Los objetos que deban eliminarse tras ser utilizados deberían implementar la interfaz
System.IDisposable (escribiendo en el método Dispose todo aquello que haya de
realizarse para liberar un objeto). El método Dispose siempre se invoca al terminar una
sentencia using:
Código no seguro
En ocasiones necesitamos tener un control total sobre la ejecución de nuestro código
(cuestiones de rendimiento, compatibilidad con código existente, uso de DLLs...), por lo
que C# nos da la posibilidad de marcar fragmentos de código como código no seguro
(unsafe) y así poder emplear C/C++ de forma nativa: punteros, aritmética de punteros,
operadores -> y *, ... sin recolección de basura. La instrucción stackalloc reserva
memoria en la pila de manera similar a como malloc lo hace en C o new en C++.
struct MiStruct
{
private unsafe int * pX; // Campo de tipo puntero
no seguro
...
}
unsafe
{
// Instrucciones que usan punteros
}
En caso de que la compilación se vaya a realizar a través de Visual Studio .NET, la forma
de indicar que se desea compilar código inseguro es activando la casilla Proyecto |
Propiedades de (proyecto) | Propiedades de Configuración | Generar | Permitir bloques
de código no seguro | True.
class StackallocApp
{
public unsafe static void Main()
{
const int TAM = 10;
int * pt = stackalloc int[TAM];
Console.ReadLine ();
}
}
Para asegurarnos de que el recolector de basura no mueve nuestros datos tendremos que
utilizar la sentencia fixed. El recolector puede mover los datos de tipo referencia, por lo
que si un puntero contiene la dirección de un dato de tipo referencia podría apuntar a una
dirección incorrecta después de que el recolector de basura trabajara. Si un conjunto de
instrucciones se encierra en un bloque fixed se previene al recolector de basura para que
no mueva el objeto al que se referencia mientras dura el bloque fixed.
Esta capacidad tiene su coste: al emplear punteros, el código resultante es inseguro ya que
éste no se puede verificar. De modo que tendremos que extremar las precauciones si alguna
vez tenemos que usar esta capacidad del lenguaje C#.
Preprocesador
C# proporciona una serie de directivas de preprocesamiento con distintas funciones.
Aunque se le sigue llamando preprocesador (como en C o C++), el preprocesador no es
independiente del compilador y se han eliminado algunas directivas como #include (para
mejorar los tiempos de compilación, se utiliza el esquema de lenguajes como Java o Delphi
en lugar de los ficheros de cabecera típicos de C/C++) o las macros de #define (para
mejorar la claridad del código).
Directiva Descripción
#define, #undef
Definición de símbolos para la
compilación condicional.
#if, #elif, #else,
#endif Compilación condicional.
#error, #warning Emisión de errores y avisos.
#region, #end Delimitación de regiones.
#line Especificación de números de línea.
Aserciones
Las aserciones nos permiten mejorar la calidad de nuestro código. Esencialmente, las
aserciones no son más que pruebas de unidad que están incluidas en el propio código
fuente. Las aserciones nos permiten comprobar precondiciones, postcondiciones e
invariantes. Las aserciones sólo se habilitan cuando se compila el código para depurarlo, de
forma que su correcto funcionamiento se compruebe continuamente. Cuando distribuyamos
nuestras aplicaciones, las aserciones se eliminan para no empeorar la eficiencia de nuestro
código.
El método Assert() comprueba una condición y muestra un mensaje si ésta es falsa. Puede
emplearse cualquiera de estas versiones:
Documentación
A los programadores no les suele gustar documentar código, por lo que resulta conveniente
suministrar un mecanismo sencillo que les permita mantener su documentación actualizada.
Al estilo de doxygen o Javadoc, el compilador de C# es capaz de generarla
automáticamente a partir de los comentarios que el progamador escriba en los ficheros de
código fuente. Los comentarios a partir de los cuales se genera la documentación se
escriben en XML.
El hecho de que la documentación se genere a partir de los fuentes permite evitar que se
tenga que trabajar con dos tipos de documentos por separado (fuentes y documentación)
que deban actualizarse simultáneamente para evitar incosistencias entre ellos derivadas de
que evolucionen de manera separada ya sea por pereza o por error.
El compilador genera la documentación en XML con la idea de que sea fácilmente legible
para cualquier aplicación. Para facilitar su legibilidad a humanos bastaría añaderle una hoja
de estilo XSL o usar alguna aplicación específica encargada de leerla y mostrarla de una
forma más cómoda para humanos.
Los comentarios XML se denotan con una barra triple (///) y nos permiten generar la
documentación del código cuando compilamos con la opción /doc.
El formato de los comentarios viene definido en un esquema XML, si bien podemos añadir
nuestras propias etiquetas para personalizar la documentación de nuestras aplicaciones.
Algunas de las etiquetas predefinidas se verifican cuando generamos la documentación
(parámetros, excepciones, tipos...).
Estos comentarios han preceder las definiciones de los elementos a documentar. Estos
elementos sólo pueden ser definiciones de miembros, ya sean tipos de datos (que son
miembros de espacios de nombres) o miembros de tipos datos, y han de colocarse incluso
incluso antes que sus atributos.
using System;
namespace Geometria {
/// <summary>
/// Clase Punto.
/// </summary>
/// <remarks>
/// Caracteriza a los puntos de un espacio
bidimensional.
/// Tiene múltiples aplicaciones....
/// </remarks>
class Punto {
/// <summary>
/// Campo que contiene la coordenada X de un punto
/// </summary>
/// <remarks>
/// Es de solo lectura
/// </remarks>
/// <summary>
/// Campo que contiene la coordenada Y de un punto
/// </summary>
/// <remarks>
/// Es de solo lectura
/// </remarks>
public readonly uint Y;
/// <summary>
/// Constructor de la clase
/// </summary>
/// <param name="x">Coordenada x</param>
/// <param name="y">Coordenada y</param>
/// <summary>
/// Clase Cuadrado. Los objetos de esta clase son
polígonos
/// cerrados de cuatro lados de igual longitud y que
/// forman ángulos rectos.
/// </summary>
/// <remarks>
/// Los cuatro vértices pueden numerarse de manera
que
/// el vértice 1 es el que tiene los menores
valores de
/// las coordenadas X e Y.
///
/// Los demás vértices se numeran a partir de éste
recorriendo
/// el cuadrado en sentido antihorario.
/// </remarks>
class Cuadrado {
/// <summary>
/// Campo que contiene las coordenadas del vértice
1.
/// </summary>
///
protected Punto vertice1;
/// <summary>
/// Campo que contiene la longitud del lado.
/// </summary>
protected uint lado;
/// <summary>
/// Constructor de la clase.
/// Construye un cuadrado a partir del vértice 1 y
de
/// la longitud del lado.
/// </summary>
/// <param name="vert1">Coordenada del vértice
1</param>
/// <param name="lado">Longitud del lado</param>
///
public Cuadrado(Punto vert1, uint lado) {
this.vertice1=vert1;
this.lado=lado;
}
/// <summary>
/// Constructor de la clase.
/// Construye un cuadrado a partir de los vértices
1 y 3.
/// </summary>
/// <param name="vert1">Coordenada del vértice
1</param>
/// <param name="vert3">Coordenada del vértice
3</param>
/// <remarks>
/// Habría que comprobar si las componentes del
vértice 3
/// son mayores o menores que las del vértice1.
/// Vamos a presuponer que las componentes del
vértice 3 son
/// siempre mayores que las del uno.
/// </remarks>
public Cuadrado(Punto vert1, Punto vert3) {
this.vertice1=vert1;
this.lado=(uint) Math.Abs(vert3.X-vert1.X);
}
/// <summary>
/// Propiedad que devuelve el punto que representa
a
/// las coordenadas del vértice 1.
/// </summary>
public Punto Vertice1 {
get {
return this.vertice1;
}
}
/// <summary>
/// Propiedad que devuelve el punto que representa
a
/// las coordenadas del vértice 2.
/// </summary>
public Punto Vertice2 {
get {
Punto p=new Punto(this.vertice1.X +
this.lado,this.vertice1.Y);
return p;
}
}
......
namespace PruebaGeometria {
using Geometria;
class GeometriaApp {
....