Tema 16: Instrucciones
Concepto de instrucción
Toda acción que se pueda realizar en el cuerpo de un
método, como definir variables locales, llamar a
métodos, asignaciones y muchas cosas más que veremos
a lo largo de este tema, son instrucciones.
Las instrucciones se agrupan formando bloques de
instrucciones, que son listas de instrucciones encerradas entre
llaves que se ejecutan una tras otra. Es decir, la sintaxis que se
sigue para definir un bloque de instrucciones es:
{ <listaInstrucciones> }
Toda variable que se defina dentro de un bloque de instrucciones
sólo existirá dentro de dicho bloque. Tras él
será inaccesible y podrá ser destruida por el
recolector de basura. Por ejemplo, este código no es
válido:
public void f();
{
{ int b; }
b = 1; // ERROR: b no existe fuera del bloque donde se declaró.
}
Los bloques de instrucciones pueden anidarse, aunque si dentro de
un bloque interno definimos una variable con el mismo nombre que
otra definida en un bloque externo se considerará que se ha
producido un error, ya que no se podrá determinar a
cuál de las dos se estará haciendo referencia cada
vez que se utilice su nombre en el bloque interno.
Instrucciones
básicas
Definiciones de
variables locales
En el Tema 7:Variables y tipos de datos ya se vió que
las variables locales son variables que se definen en el
cuerpo de los métodos y sólo son accesibles desde
dichos cuerpos. Recuérdese que la sintaxis explicada para
definirlas era la siguiente:
<modificadores> <tipoVariable> <nombreVariable> = <valor>;
También se vió que podían definirse varias
variables en una misma instrucción separando sus pares
nombre-valor mediante comas. Por ejemplo:
int a=5, b, c=-1;
Asignaciones
Una asignación es simplemente una instrucción
mediante la que se indica un valor a almacenar en un dato. La
sintaxis usada para ello es:
<destino> = <origen>;
En temas previos ya se han dado numerosos ejemplos de cómo
hacer esto, por lo que no es necesario hacer ahora mayor
hincapié en ello.
Llamadas a
métodos
En el Tema 8: Métodos ya se explicó que una
llamada a un método consiste en solicitar la
ejecución de sus instrucciones asociadas dando a sus
parámetros ciertos valores. Si el método a llamar es
una método de objeto, la sintaxis usada para ello es:
<objeto>.<nombreMétodo>(<valoresParámetros>);
Y si el método a llamar es un método de tipo,
entonces la llamada se realiza con:
<nombreTipo>.<nombreMétodo>(<valoresParámetros>);
Recuérdese que si la llamada al método de tipo se
hace dentro de la misma definición de tipo donde el
método fue definido, la sección
<nombreTipo>. de la sintaxis es opcional.
Instrucción
nula
La instrucción nula es una instrucción que no
realiza nada en absoluto. Su sintaxis consiste en escribir un
simple punto y coma para representarla. O sea, es:
;
Suele usarse cuando se desea indicar explícitamente que no
se desea ejecutar nada. Usarla es útil para facilitar la
legibilidad del código o, como veremos más adelante
en el tema, porque otras instrucciones la necesitan para indicar
cuándo en algunos de sus bloques de instrucciones
componentes no se ha de realizar ninguna acción.
Instrucciones
condicionales
Las instrucciones condicionales son instrucciones que
permiten ejecutar bloques de instrucciones sólo si se da una
determinada condición. En los siguientes subapartados de
este epígrafe se describen cuáles son las
instrucciones condicionales disponibles en C#
Instrucción
if
La instrucción
if permite ejecutar ciertas
instrucciones sólo si de da una determinada
condición. Su sintaxis de uso es la sintaxis:
if (<condición>)
<instruccionesIf>
else
<instruccionesElse>
El significado de esta instrucción es el siguiente: se
evalúa la expresión <condición>, que ha
de devolver un valor lógico. Si es cierta (devuelve
true) se ejecutan las <instruccionesIf>, y si es falsa
(false) se ejecutan las <instruccionesElse> La rama
else es opcional, y si se omite y la condición es
falsa se seguiría ejecutando a partir de la
instrucción siguiente al if. En realidad, tanto
<instruccionesIf> como <instruccionesElse> pueden ser
una única instrucción o un bloque de instrucciones.
Un ejemplo de aplicación de esta instrucción es esta
variante del HolaMundo:
using System;
class HolaMundoIf
{
public static void Main(String[] args)
{
if (args.Length > 0)
Console.WriteLine("¡Hola {0}!", args[0]);
else
Console.WriteLine("¡Hola mundo!");
}
}
Si ejecutamos este programa sin ningún argumento veremos que
el mensaje que se muestra es ¡Hola Mundo!, mientras que si lo
ejecutamos con algún argumento se mostrará un mensaje
de bienvenida personalizado con el primer argumento indicado.
Instrucción
switch
La instrucción
switch permite ejecutar unos u
otros bloques de instrucciones según el valor de una cierta
expresión. Su estructura es:
switch (<expresión>)
{
case <valor1>: <bloque1>
<siguienteAcción>
case <valor2>: <bloque2>
<siguienteAcción>
...
default: <bloqueDefault>
<siguienteAcción>
}
El significado de esta instrucción es el siguiente: se
evalúa <expresión>. Si su valor es
<valor1> se ejecuta el <bloque1>, si es <valor2>
se ejecuta <bloque2>, y así para el resto de valores
especificados. Si no es igual a ninguno de esos valores y se
incluye la rama default, se ejecuta el
<bloqueDefault>; pero si no se incluye se pasa directamente a
ejecutar la instrucción siguiente al switch
.
Los valores indicados en cada rama del switch han de ser
expresiones constantes que produzcan valores de algún tipo
básico entero, de una enumeración, de tipo
char o de tipo string. Además, no puede haber
más de una rama con el mismo valor.
En realidad, aunque todas las ramas de un switch son
opcionales siempre se ha de incluir al menos una. Además, la
rama default no tiene porqué aparecer la
última si se usa, aunque es recomendable que lo haga para
facilitar la legibilidad del código.
El elemento marcado como <siguienteAcción> colocado
tras cada bloque de instrucciones indica qué es lo que ha de
hacerse tras ejecutar las instrucciones del bloque que lo preceden.
Puede ser uno de estos tres tipos de instrucciones:
goto case <valori>;
goto default;
break;
Si es un goto case indica que se ha de seguir ejecutando el
bloque de instrucciones asociado en el switch a la rama del
<valori> indicado, si es un goto default indica que se
ha de seguir ejecutando el bloque de instrucciones de la rama
default, y si es un break indica que se ha de
seguir ejecutando la instrucción siguiente al switch.
El siguiente ejemplo muestra cómo se utiliza switch:
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");
}
}
Este programa reconoce ciertos nombres de personas que se le pueden
pasar como argumentos al lanzarlo y les saluda de forma especial.
La rama default se incluye para dar un saludo por defecto a
las personas no reconocidas.
Para los programadores habituados a lenguajes como C++ es
importante resaltarles el hecho de que, a diferencia de dichos
lenguajes, C# obliga a incluir una sentencia break o una
sentencia goto case al final de cada rama del switch
para evitar errores comunes y difíciles de detectar causados
por olvidar incluir break; al final de alguno de estos
bloques y ello provocar que tras ejecutarse ese bloque se ejecute
también el siguiente.
Instrucciones
iterativas
Las instrucciones iterativas son instrucciones que permiten
ejecutar repetidas veces una instrucción o un bloque de
instrucciones mientras se cumpla una condición. Es decir,
permiten definir bucles donde ciertas instrucciones se ejecuten
varias veces. A continuación se describen cuáles son
las instrucciones de este tipo incluidas en C#.
Instrucción
while
La instrucción while permite ejecutar un bloque
de instrucciones mientras se de una cierta instrucción. Su
sintaxis de uso es:
while (<condición>)
<instrucciones>
Su significado es el siguiente: Se evalúa la
<condición> indicada, que ha de producir un valor
lógico. Si es cierta (valor lógico true) se
ejecutan las <instrucciones> y se repite el proceso de
evaluación de <condición> y ejecución de
<instrucciones> hasta que deje de serlo. Cuando sea falsa
(false) se pasará a ejecutar la instrucción
siguiente al while. En realidad <instrucciones> puede
ser una única instrucción o un bloque de
instrucciones.
Un ejemplo cómo utilizar esta instrucción es el
siguiente:
using System;
class HolaMundoWhile
{
public static void Main(String[] args)
{
int actual = 0;
if (args.Length > 0)
while (actual < args.Length)
{
Console.WriteLine("¡Hola {0}!", args[actual]);
actual = actual + 1;
}
else
Console.WriteLine("¡Hola mundo!");
}
}
En este caso, si se indica más de un argumento al llamar al
programa se mostrará por pantalla un mensaje de saludo para
cada uno de ellos. Para ello se usa una variable actual que
almacena cuál es el número de argumento a mostrar en
cada ejecución del while. Para mantenerla siempre
actualizada lo que se hace es aumentar en una unidad su valor tras
cada ejecución de las <instrucciones> del bucle.
Por otro lado, dentro de las <instrucciones> de un
while pueden usarse dos instrucciones especiales:
-
break;: Indica que se ha de abortar la ejecución
del bucle y continuarse ejecutando por la
instrucción siguiente al while.
-
continue;: Indica que se ha de abortar la
ejecución de las <instrucciones> y reevaluarse la
<condición> del bucle, volviéndose a
ejecutar la <instrucciones> si es cierta o
pasándose a ejecutar la instrucción siguiente
alwhile si es falsa.
Instrucción
do...while
La instrucción do...while es una variante
del while que se usa así:
do
<instrucciones>
while(<condición>);
La única diferencia del significado de do...while
respecto al de while es que en vez de evaluar primero la
condición y ejecutar <instrucciones> sólo si es
cierta, do...while primero ejecuta las <instrucciones>
y luego mira la <condición> para ver si se ha de
repetir la ejecución de las mismas. Por lo demás
ambas instrucciones son iguales, e incluso también puede
incluirse break; y continue; entre las
<instrucciones> del do...while.
do ... while está especialmente destinado para los
casos en los que haya que ejecutar las <instrucciones> al
menos una vez aún cuando la condición sea falsa desde
el principio., como ocurre en el siguiente ejemplo:
using System;
class HolaMundoDoWhile
{
public static void Main()
{
String leído;
do
{
Console.WriteLine("Clave: ");
leído = Console.ReadLine();
}
while (leído != "José");
Console.WriteLine("Hola José");
}
}
Este programa pregunta al usuario una clave y mientras no
introduzca la correcta (José) no continuará
ejecutándose. Una vez que introducida correctamente
dará un mensaje de bienvenida al usuario.
Instrucción
for
La instrucción for es una variante de
while que permite reducir el código necesario para
escribir los tipos de bucles más comúnmente usados en
programación. Su sintaxis es:
for (<inicialización>; <condición>; <modificación>)
<instrucciones>
El significado de esta instrucción es el siguiente: se
ejecutan las instrucciones de <inicialización>, que
suelen usarse para definir e inicializar variables que luego se
usarán en <instrucciones>. Luego se evalúa
<condición>, y si es falsa se continúa
ejecutando por la instrucción siguiente al
for;
mientras que si es cierta se ejecutan las <instrucciones>
indicadas, luego se ejecutan las instrucciones de
<modificación> -que como su nombre indica suelen
usarse para modificar los valores de variables que se usen en
<instrucciones>- y luego se reevalúa
<condición> repitiéndose el proceso hasta que
ésta última deje de ser cierta.
En <inicialización> puede en realidad incluirse
cualquier número de instrucciones que no tienen
porqué ser relativas a inicializar variables o modificarlas,
aunque lo anteriro sea su uso más habitual. En caso de ser
varias se han de separar mediante comas (,), ya que el
carácter de punto y coma (;) habitualmente usado para
estos menesteres se usa en el for para separar los
bloques de <inicialización>, <condición>
y <modificación>. Además, la instrucción
nula no se puede usar en este caso y tampoco pueden combinarse
definiciones de variables con instrucciones de otros tipos.
Con <modificación> pasa algo similar, ya que puede
incluirse código que nada tenga que ver con modificaciones
pero en este caso no se pueden incluir definiciones de variables.
Como en el resto de instrucciones hasta ahora vistas, en
<instrucciones> puede ser tanto una única
instrucción como un bloque de instrucciones. Además,
las variables que se definan en <inicialización>
serán visibles sólo dentro de esas
<instrucciones>
La siguiente clase es equivalente a la clase HolaMundoWhile ya
vista solo que hace uso del for para compactar más su
código:
using System;
class HolaMundoFor
{
public static void Main(String[] args)
{
if (args.Length > 0)
for (int actual = 0; actual < args.Length; actual++)
Console.WriteLine("¡Hola {0}!", args[actual]);
else
Console.WriteLine("¡Hola mundo!");
}
}
Al igual que con while, dentro de
las <instrucciones> del for también pueden
incluirse instrucciones continue; y break; que puedan
alterar el funcionamiento normal del bucle.
Instrucción
foreach
La instrucción foreach es una variante
del for pensada especialmente para compactar la escritura de
códigos donde se realice algún tratamiento a todos
los elementos de una colección, que suele un uso muy
habitual de for en los lenguajes de programación que
lo incluyen. La sintaxis que se sigue a la hora de escribir esta
instrucción foreach es:
foreach (<tipoElemento> <elemento> in <colección>)
<instrucciones>
El significado de esta instrucción es muy sencillo: se
ejecutan <instrucciones> para cada uno de los elementos
de la <colección> indicada. <elemento> es una
variable de sólo lectura de tipo <tipoElemento> que
almacenará en cada momento el elemento de la
colección que se esté procesando y que podrá
ser accedida desde <instrucciones>.
Es importante señalar que <colección> no puede
valer null porque entonces saltaría una
excepción de tipo System.NullReferenceException, y que
<tipoElemento> ha de ser un tipo cuyos objetos puedan
almacenar los valores de los elementos de <colección>
En tanto que una tabla se considera que es una colección, el
siguiente código muestra cómo usar for para
compactar aún más el código de la clase
HolaMundoFor anterior:
using System;
class HolaMundoFoeach
{
public static void Main(String[] args)
{
if (args.Length > 0)
foreach(String arg in args)
Console.WriteLine("¡Hola {0}!", arg);
else
Console.WriteLine("¡Hola mundo!");
}
}
En general, se considera que una colección es todo aquel
objeto que implemente la
interfaz System.Collections.IEnumerable. Esta interfaz
está definida en la BCL así:
interface IEnumerable
{
IEnumerator GetEnumerator();
}
El objeto de interfaz System.Collections.IEnumerator devuelto
ha de ser un enumerador que permita recorrer los elementos de la
<colección>. Dicha interfaz está así
predefinida:
interface IEnumerator
{
object Current {get;}
bool MoveNext();
void Reset();
}
El método Reset() ha de implementarse de modo que
devuelva el enumerador reiniciado a un estado inicial donde
aún no referencie ni siquiera al primer elemento de la
colección sino que sea necesario llamar a MoveNext()
para que lo haga.
El método MoveNext() se ha de implementar de modo que
haga que el enumerador pase a apuntar al siguiente elemento de la
colección y devuelva un booleano que indique si tras avanzar
se ha alcanzado el final de la colección.
La propiedad Current se ha de implementar de modo que
devuelva siempre el elemento de la colección al que el
enumerador esté referenciando. Si se intenta
leer Current habiéndose ya recorrido toda la
colección o habiéndose reiniciado la colección
y no habiéndose colocado en su primer elemento con
MoveNext(), se ha de producir una excepción de
tipo System.Exception.SystemException.InvalidOperationException
Otra forma de conseguir que foreach considere que un objeto
es una colección válida consiste en hacer que dicho
objeto siga el patrón de colección. Este
patrón consiste en definir el tipo del objeto de modo que
sus objetos cuenten con un método público
GetEnumerator() que devuelva un objeto no nulo que cuente
con una propiedad pública llamada Current que permita
leer el elemento actual y con un método
público bool MoveNext() que permita cambiar el
elemento actual por el siguiente y devuelva false sólo
cuando se haya llegado al final de la colección.
El siguiente ejemplo muestra ambos tipos de implementaciones:
using System;
using System.Collections;
class Patron
{
private int actual = -1;
public Patron GetEnumerator()
{
return this;
}
public int Current
{
get {return actual;}
}
public bool MoveNext()
{
bool resultado = true;
actual++;
if (actual==10)
resultado = false;
return resultado;
}
}
class Interfaz:IEnumerable,IEnumerator
{
private int actual = -1;
public object Current
{
get {return actual;}
}
public bool MoveNext()
{
bool resultado = true;
actual++;
if (actual==10)
resultado = false;
return resultado;
}
public IEnumerator GetEnumerator()
{
return this;
}
public void Reset()
{
actual = -1;
}
}
class Principal
{
public static void Main()
{
Patron obj = new Patron();
Interfaz obj2 = new Interfaz();
foreach (int elem in obj)
Console.WriteLine(elem);
foreach (int elem in obj2)
Console.WriteLine(elem);
}
}
El tipo System.Array implementa la
interfaz System.Collectiones.IEnumerator, por lo que todas
las tablas podrán ser usadas recorridas con foreach.
Si la tabla a recorrer es multidimensional, sus elementos se
recorrerán en orden como muestra este ejemplo:
int[,] tabla = { {1,2}, {3,4} };
foreach (int elemento in tabla)
Console.WriteLine(elemento);
La salida por pantalla del fragmento de código anterior
será:
1
2
3
4
La utilidad de implementar el patrón colección en
lugar de la interfaz IEnumerable es que así no es
necesario que Current devuelva siempre un object, sino
que puede devolver objetos de tipos más concretos y gracias
a ello puede detectarse al compilar si el <tipoElemento>
indicado puede o no almacenar los objetos de la colección.
Por ejemplo, si en el ejemplo anterior sustituimos en el
último foreach el <tipoElemento> indicado por
Patrón, el código seguirá compilando pero al
ejecutarlo saltará una excepción
System.InvalidCastException. Sin embargo, si la
sustitución se hubiese hecho en el penúltimo
foreach, entonces el código directamente no
compilaría y se nos informaría de un error debido a
que los objetos int no son convertibles en objetos
Patrón.
También hay que tener en cuenta que la
comprobación de tipos que se realiza en tiempo de
ejecución si el objeto sólo implementó la
interfaz IEnumerable es muy estricta, en el sentido de que si
en el ejemplo anterior sustituimos el <tipoElemento> del
último foreach por byte también se
lanzará la excepción al no ser los objetos de
tipo int implícitamente convertibles en
bytes sino sólo a través del operador
() Sin embargo, cuando se sigue el patrón de
colección las comprobaciones de tipo no son tan estrictas y
entonces sí que sería válido
sustituir int por byte en <tipoElemento>.
El problema de sólo implementar el patrón
colección es que este es una característica propia de
C# y con las instrucciones foreach (o equivalentes) de
lenguajes que no lo soporten no se podría recorrer
colecciones que sólo siguiesen este patrón. Una
solución en estos casos puede ser hacer que el tipo del
objeto colección implemente tanto la interfaz
IEnumerable como el patrón colección.
Obviamente esta interfaz debería implementarse
explícitamente para evitarse conflictos derivados de que sus
miembros tengan signaturas coincidentes con las de los miembros
propios del patrón colección.
Si un objeto de un tipo colección implementa tanto la
interfaz IEnumerable como el patrón de
colección, entonces en C# foreach usará
el patrón colección para recorrerlo.
Instrucciones de
excepciones
Concepto de
excepción.
Las excepciones son el mecanismo recomendado en la
plataforma .NET para la propagación de errores que se
produzcan durante la ejecución de las aplicaciones
(divisiones por cero, intentos de lectura de archivos
dañados, etc.) Básicamente una excepción es un
objeto derivado de System.Exception que se genera cuando en
tiempo de ejecución se produce algún error y
que contiene información sobre el mismo.
Tradicionalmente, el sistema que en otros lenguajes y plataformas
se ha venido usando para informar estos errores consistía
simplemente en hacer que los métodos en cuya
ejecución pudiesen producirse devolvieran códigos que
informasen sobre si se han ejecutado correctamente o, en caso
contrario, sobre cuál fue el error producido. Sin embargo,
las excepciones proporcionan las siguientes ventajas frente a dicho
sistema:
-
Claridad: El uso de códigos especiales para informar
de error suele dificultar la legibilidad del fuente en tanto que se
mezclan las instrucciones propias de la lógica del mismo con
las instrucciones propias del tratamiento de los errores que
pudiesen producirse durante su ejecución. Por ejemplo:
int resultado = obj.Método();
if (resultado == 0) // Sin errores al ejecutar obj.Método();
{...}
else if (resultado == 1) // Tratamiento de error de código 1
{...}
else if (resultado == 2) // Tratamiento de error de código 2
...
Como se verá, utilizando excepciones es posible escribir el
código como si nunca se fuesen a producir errores y
dejar en una zona aparte todo el código de tratamiento de
errores, lo que contribuye a facilitar la legibilidad de los
fuentes.
-
Más información: A partir del valor de un
código de error puede ser difícil deducir las causas
del mismo y conseguirlo muchas veces implica tenerse que consultar
la documentación que proporcionada sobre el método
que lo provocó, que puede incluso que no especifique
claramente su causa.
Por el contrario, una excepción es un objeto que cuenta con
campos que describen las causas del error y a cuyo tipo suele
dársele un nombre que resuma claramente su causa. Por
ejemplo, para informar errores de división por cero se
suele utilizar una excepción predefinida de tipo
DivideByZeroException en cuyo campo Message se
detallan las causas del error producido
-
Tratamiento asegurado: Cuando se utilizan códigos de
error nada obliga a tratarlos en cada llamada al método que
los pueda producir, e ignorarlos puede provocar más adelante
en el código comportamientos inesperados de causas
difíciles de descubrir.
Cuando se usan excepciones siempre se asegura que el programador
trate toda excepción que pueda producirse o que, si no lo
hace, se aborte la ejecución de la aplicación
mostrándose un mensaje indicando dónde se ha
producido el error.
Ahora bien, tradicionalmente en lenguajes como C++ el uso de
excepciones siempre ha tenido las desventajas respecto al uso de
códigos de error de complicar el compilador y dar lugar a
códigos más lentos y difíciles de optimizar en
los que tras cada instrucción que pudiese producir
excepciones el compilador debe introducir las comprobaciones
necesarias para detectarlas y tratarlas así como para
comprobar que los objetos creados sean correctamente destruidos si
se producen.
Sin embargo, en la plataforma .NET desaparacen los problemas de
complicar el compilador y dificultar las optimizaciones ya que que
es el CLR quien se encarga de detectar y tratar las excepciones y
es su recolector de basura quien se encarga asegurar la correcta
destrucción de los objetos. Obviamente el código
seguirá siendo algo más lento, pero es un
pequeño sacrificio que merece la pena hacer en tanto que
ello asegura que nunca se producirán problemas
difíciles de detectar derivados de errores ignorados.
La clase
System.Exception
Como ya se ha dicho, todas las excepciones derivan de un tipo
predefinido en la BCL llamado System.Exception. Los
principales miembros que heredan de éste son:
-
string Message {virtual get;}: Contiene un mensaje
descriptivo de las causas de la excepción. Por defecto
este mensaje es una cadena vacía ("")
-
Exception InnerException {virtual get;}: Si una
excepción fue causada como consecuencia de otra, esta
propiedad contiene el objeto System.Exception que
representa a la excepción que la causó. Así
se pueden formar cadenas de excepciones de cualquier longitud.
Si se desea obtener la última excepción de la
cadena es mejor usar el método virtual Exception
GetBaseException()
-
string StackTrace {virtual get;}: Contiene la pila de
llamadas a métodos que se tenía en el momento en
que se produjo la excepción. Esta pila es una cadena con
información sobre cuál es el método en que
se produjo la excepción, cuál es el método
que llamó a este, cuál es el que llamó a
ese otro, etc.
-
string Source {virtual get; virtual set;}: Almacena
información sobre cuál fue la aplicación u
objeto que causó la excepción.
-
MethodBase TargetSite {virtual get;}: Almacena
cuál fue el método donde se produjo la
excepción en forma de objeto
System.Reflection.MethodBase. Puede consultar la
documentación del SDK si desea cómo obtener
información sobre las características del
método a través del objeto MethodBase.
-
string HelpLink {virtual get;}: Contiene una cadena con
información sobre cuál es la URI donde se puede
encontrar información sobre la excepción. El valor
de esta cadena puede establecerse con virtual Exception
SetHelpLink (string URI), que devuelve la excepción
sobre la que se aplica pero con la URI ya actualizada.
Para crear objetos de clase System.Exception se puede usar
los constructores:
Exception()
Exception(string msg)
Exception(string msg, Exception causante)
El primer constructor crea una excepción cuyo valor para
Message será "" y no causada por ninguna
otra excepción (InnerException valdrá
null) El segundo la crea con el valor indicado para
Message, y el último la crea con además la
excepción causante indicada.
En la práctica, cuando se crean nuevos tipos derivados de
System.Exception no se suele redefinir sus miembros ni
añadirles nuevos, sino que sólo se hace la
derivación para distinguir una excepciones de otra por el
nombre del tipo al que pertenecen. Ahora bien, es conveniente
respetar el convenio de darles un nombre acabado en Exception
y redefinir los tres constructores antes comentados.
Excepciones predefinidas
comunes
En el espacio de nombres System de la BCL hay predefinidas
múltiples excepciones derivadas de System.Exception
que se corresponden con los errores más comunes que pueden
surgir durante la ejecución de una aplicación. En la
Tabla 8 se recogen algunas:
| Tipo de la excepción |
Causa de que se produzca la excepción |
|
ArgumentException
|
Pasado argumento no válido (base de excepciones de
argumentos)
|
|
ArgumentNullException
|
Pasado argumento nulo
|
|
ArgumentOutOfRangeException
|
Pasado argumento fuera de rango
|
|
ArrayTypeMistmatchException
|
Asignación a tabla de elemento que no es de su tipo
|
|
COMException
|
Excepción de objeto COM
|
|
DivideByZeroException
|
División por cero
|
|
IndexOutOfRangeException
|
Índice de acceso a elemento de tabla fuera del
rango válido (menor que cero o mayor que el
tamaño de la tabla)
|
|
InvalidCastException
|
Conversión explícita entre tipos no
válida
|
|
InvalidOperationException
|
Operación inválida en estado actual del
objeto
|
|
InteropException
|
Base de excepciones producidas en comunicación con
código inseguro
|
|
NullReferenceException
|
Acceso a miembro de objeto que vale null
|
|
OverflowException
|
Desbordamiento dentro de contexto donde se ha de comprobar
los desbordamientos (expresión constante,
instrucción checked, operanción checked u
opción del compilador /checked)
|
|
OutOfMemoryException
|
Falta de memoria para crear un objeto con new
|
|
SEHException
|
Excepción SHE del API Win32
|
|
StackOverflowException
|
Desbordamiento de la pila, generalmente debido a un
excesivo número de llamadas recurrentes.
|
|
TypeInizializationException
|
Ha ocurrido alguna excepción al inicializar los
campos estáticos o el constructor estático
de un tipo. En InnerException se indica cuál es.
|
Tabla 8: Excepciones
predefinidas de uso frecuente
Obviamente, es conveniente que si las aplicaciones que escribamos
necesiten lanzar excepciones relativas a errores de los tipos
especificados en la Tabla 8, lancen precisamente las
excepciones indicadas en esa tabla y no cualquier otra - ya
sea definida por nosotros mismos o predefinida en la BCL con otro
significado.
Lanzamiento de
excepciones. Instrucción throw
Para informar de un error no basta con crear un objeto del tipo de
excepción apropiado, sino que también hay
pasárselo al mecanismo de propagación de excepciones
del CLR. A esto se le llama lanzar la excepción, y
para hacerlo se usa la siguiente instrucción:
throw <objetoExcepciónALanzar>;
Por ejemplo, para lanzar una excepción de
tipo DivideByZeroException se podría hacer:
throw new DivideByZeroException();
Si el objeto a lanzar vale null, entonces se
producirá una NullReferenceException que será
lanzada en vez de la excepción indicada en la
instrucción throw.
Captura de excepciones.
Instrucción try
Una vez lanzada una excepción es posible escribir
código que es encarge de tratarla. Por defecto, si este
código no se escribe la excepción provoca que la
aplicación aborte mostrando un mensaje de error en el que se
describe la excepción producida (información de su
propiedad Message) y dónde se ha producido
(información de su propiedad StackTrace) Así,
dado el siguiente código fuente de ejemplo:
using System;
class PruebaExcepciones
{
static void Main()
{
A obj1 = new A();
obj1.F();
}
}
class A
{
public void F()
{
G();
}
static public void G()
{
int c = 0;
int d = 2/c;
}
}
Al compilarlo no se detectará ningún error ya que al
compilador no le merece la pena calcular el valor de c en tanto que
es una variable, por lo que no detectará que dividir 2/c no
es válido. Sin embargo, al ejecutarlo se intentará
dividir por cero en esa instrucción y ello provocará
que aborte la aplicación mostrando el siguiente mensaje:
Unhandled Exception: System.DivideByZeroException: Attempted to divide by zero.
at PruebaExcepciones.Main()
Como se ve, en este mensaje se indica que no se ha tratado una
excepción de división por cero (tipo
DivideByZeroException) dentro del código del
método Main() del tipo PruebaExcepciones. Si al compilar el
fuente hubiésemos utilizado la opción /debug,
el compilador habría creado un fichero .pdb con
información extra sobre las instrucciones del ejecutable
generado que permitiría que al ejecutarlo se mostrase un
mensaje mucho más detallado con información sobre la
instrucción exacta que provocó la excepción,
la cadena de llamadas a métodos que llevaron a su
ejecución y el número de línea que cada una
ocupa en el fuente:
Unhandled Exception: System.DivideByZeroException: Attempted to divide by zero.
at A.G() in E:\c#\Ej\ej.cs:line 22
at A.F() in E:\c#\Ej\ej.cs:line 16
at PruebaExcepciones.Main() in E:\c#\Ej\ej.cs:line 8
Si se desea tratar la excepción hay que encerrar la
división dentro de una instrucción try
con la siguiente sintaxis:
try
<instrucciones>
catch (<excepción1>)
<tratamiento1>
catch (<excepción2>)
<tratamiento2>
...
finally
<instruccionesFinally>
El significado de try es el siguiente: si durante la
ejecución de las <instrucciones> se lanza una
excepción de tipo <excepción1> (o alguna
subclase suya) se ejecutan las instrucciones <tratamiento1>,
si fuese de tipo <excepción2> se ejecutaría
<tratamiento2>, y así hasta que se encuentre una
cláusula catch que pueda tratar la excepción
producida. Si no se encontrase ninguna y la instrucción
try estuviese anidada dentro de otra, se miraría en
los catch de su try padre y se repetiría el
proceso. Si al final se recorren todos los trys padres
y no se encuentra ningún catch compatible, entonces
se buscaría en el código desde el que se llamó
al método que produjo la excepción. Si así se
termina llegando al método que inició el hilo donde
se produjo la excepción y tampoco allí se encuentra
un tratamiento apropiado se aborta dicho hilo; y si ese hilo es el
principal (el que contiene el punto de entrada) se aborta el
programa y se muestra el mensaje de error con información
sobre la excepción lanzada ya visto.
Así, para tratar la excepción del ejemplo anterior de
modo que una división por cero provoque que a d se le asigne
el valor 0, se podría reescribir G() de esta otra forma:
static public void G()
{
try
{
int c = 0;
int d = 2/c;
}
catch (DivideByZeroException)
{ d=0; }
}
Para simplificar tanto el compilador como el código generado
y favorecer la legibilidad del fuente, en los catchs se
busca siempre orden de aparación textual, por lo que para
evitar catchs absurdos no se permite definir
catchs que puedan capturar excepciones capturables por
catchs posteriores a ellos en su misma instrucción
try.
También hay que señalar que cuando en
<instrucciones> se lance una excepción que sea tratada
por un catch de algún try -ya sea de la que
contiene las <instrucciones>, de algún try
padre suyo o de alguno de los métodos que provocaron
la llamada al que produjo la excepción- se seguirá
ejecutando a partir de las instrucciones siguientes a ese
try.
El bloque finally es opcional, y si se incluye ha de hacerlo
tras todas los bloques catch. Las
<instruccionesFinally> de este bloque se ejecutarán
tanto si se producen excepciones en <instrucciones> como si
no. En el segundo caso sus instrucciones se ejecutarán tras
las <instrucciones>, mientras que en el primero lo
harán después de tratar la excepción pero
antes de seguirse ejecutando por la instrucción siguiente al
try que la trató. Si en un try no se encuentra
un catch compatible, antes de pasar a buscar en su
try padre o en su método llamante padre se
ejecutarán las <instruccionesFinally>.
Sólo si dentro de un bloque finally se lanzase una
excepción se aborta la ejecución del mismo. Dicha
excepción sería propagada al try padre o al
método llamante padre del try que contuviese el
finally.
Aunque los bloques catch y finally son opcionales,
toda instrucción try ha de incluir al menos un bloque
catch o un bloque finally.
El siguiente ejemplo resume cómo funciona la
propagación de excepciones:
using System;
class MiException:Exception {}
class Excepciones
{
public static void Main()
{
try
{
Console.WriteLine("En el try de Main()");
Método();
Console.WriteLine("Al final del try de Main()");
}
catch (MiException)
{
Console.WriteLine("En el catch de Main()");
}
finally
{
Console.WriteLine("finally de Main()");
}
}
public static void Método()
{
try
{
Console.WriteLine("En el try de Método()");
Método2();
Console.WriteLine("Al final del try de Método()");
}
catch (OverflowException)
{
Console.WriteLine("En el catch de Método()");
}
finally
{
Console.WriteLine("finally de Método()");
}
}
public static void Método2()
{
try
{
Console.WriteLine("En el try de Método2()");
throw new MiException();
Console.WriteLine("Al final del try de Método2()");
}
catch (DivideByZeroException)
{ Console.WriteLine("En el catch de Método2()"); }
finally
{ Console.WriteLine("finally de Método2()"); }
}
}
Nótese que en este código lo único que se hace
es definir un tipo nuevo de excepción llamado MiException y
llamarse en el Main() a un método llamado
Método() que llama a otro de nombre Método2() que
lanza una excepción de ese tipo. Viendo la salida de este
código es fácil ver el recorrido seguido durante la
propagación de la excepción:
En try de Main()
En try de Método()
En try de Método2()
finally de Método2
finally de Método
En catch de Main()
finally de Main()
Como se puede observar, hay muchos WriteLine() que nunca se
ejecutan ya que en cuento se lanza una excepción se sigue
ejecutando tras la instrucción siguiente al try que
la trató (aunque ejecutando antes los finally
pendientes, como se deduce de la salida del ejemplo) De hecho, el
compilador se dará cuenta que la instrucción
siguiente al throw nunca se ejecutará e
informará de ello con un mensaje de aviso.
La idea de todo este mecanismo de excepciones es evitar mezclar el
código normal con el código de tratamiento de
errores. Así, en <instrucciones> se escibiría
el código como si no se pudiesen producir errores, en las
cláusulas catch se tratarían los posibles
errores, y en la cláusula finally se incluiría
código a ejecutar tanto si produjesen errores como si no
(suele usarse para liberar recursos ocupados, como fichero o
conexiones de red abiertas)
En realidad, también es posible escribir cada
cláusula catch definiendo una variable que se
podrá usar dentro del código de tratamiento de la
misma para hacer referencia a la excepción capturada. Esto
se hace con la sintaxis:
catch (<tipoExcepción> <nombreVariable>)
{
<tratamiento>
}
Nótese que en tanto que todas las excepciones derivan de
System.Exception, para definir una cláusula
catch que pueda capturar cualquier tipo de excepción
basta usar:
catch(System.Exception <nombreObjecto>)
{
<tratamiento>
}
En realidad la sintaxis anterior sólo permite capturar las
excepciones propias de la plataforma .NET, que derivan de
System.Exception. Sin embargo, hay lenguajes como C++
que permiten lanzar excepciones no derivadas de dicha clase, y para
esos casos se ha incluido en C# una variante de catch
sí que realmente puede capturar excepciones de cualquier
tipo, tanto si derivan de System.Exception como si no.
Su sintaxis es:
catch
{
<tratamiento>
}
Como puede deducirse de su sintaxis, el problema que presenta esta
última variante de catch es que no proporciona
información sobre cuál es la excepción
capturada, por lo que a veces puede resultar poco útil y si
sólo se desea capturar cualquier excepción derivada
de System.Exception es mejor usar la sintaxis explicada
previamente a ella.
En cualquier casos, ambos tipos de cláusulas catch
sólo pueden ser escritas como la última
cláusula catch del try, ya que si no las
cláusulas catch que le siguiesen nunca
llegarían a ejecutarse debido a que las primeras
capturarían antes cualquier excepción derivada de
System.Exception.
Respecto al uso de throw, hay que señalar que hay una
forma extra de usarlo que sólo es válida dentro de
códigos de tratamiento de excepciones (códigos
<tratamientoi> de las cláusulas catch) Esta
forma de uso consiste en seguir simplemente esta sintaxis:
throw;
En este caso lo que se hace es relanzar la misma excepción
que se capturó en el bloque catch dentro de cuyo de
código de tratamiento se usa el throw; Hay que
precisar que la excepción relanzada es precisamente la
capturada, y aunque en el bloque catch se la modifique a
través de la variable que la repreesnta, la versión
relanzada será la versión original de la misma y no
la modificada.
Además, cuando se relance una excepción en un
try con cláusula finally, antes de pasar a
reprocesar la excepción en el try padre del que la
relanzó se ejecutará dicha cláusula.
Instrucciones de
salto
Las instrucciones de salto permiten ejecutar variar el orden
normal en que se ejecutan las instrucciones de un programa, que
consiste en ejecutarlas una tras otra en el mismo orden en que se
hubiesen escrito en el fuente. En los subapartados de este
epígrafe se describirán cuáles son las
instrucciones de salto incluidas en C#:
Instrucción
break
Ya se ha visto que la instrucción break
sólo puede incluirse dentro de bloques de instrucciones
asociados a instrucciones iterativas o instrucciones switch
e indica que se desea abortar la ejecución de las mismas y
seguir ejecutando a partir de la instrucción siguiente a
ellas. Se usa así:
break;
Cuando esta sentencia se usa dentro de un try con
cláusula finally, antes de abortarse la
ejecución de la instrucción iterativa o del
switch que la contiene y seguirse ejecutando por la
instrucción que le siga, se ejecutarán las
instrucciones de la cláusula finally del try.
Esto se hace para asegurar que el bloque finally se ejecute
aún en caso de salto.
Además, si dentro una cláusula finally
incluida en de un switch o de una instrucción
iterativa se usa break, no se permite que como resultado del
break se salga del finally.
Instrucción
continue
Ya se ha visto que la instrucción continue
sólo puede usarse dentro del bloque de instrucciones de una
instrucción iterativa e indica que se desea pasar a
reevaluar directamente la condición de la misma sin ejecutar
el resto de instrucciones que contuviese. La evaluación de
la condición se haría de la forma habitual: si es
cierta se repite el bucle y si es falsa se continúa
ejecutando por la instrucción que le sigue. Su sintaxis de
uso es así de sencilla:
continue;
En cuanto a sus usos dentro de sentencias try, tiene las
mismas restricciones que break: antes de salir de un
try se ejecutará siempre su bloque finally y
no es posible salir de un finally incluido dentro de una
instrucción iterativa como consecuencia de un
continue.
Instrucción
return
Esta instrucción se usa para indicar cuál es el
objeto que ha de devolver un método, y se usa
así:
return <objetoRetorno>;
La ejecución de esta instrucción provoca que se
aborte la ejecución del método dentro del que aparece
y que se devuelva el <objetoRetorno> al método que lo
llamó. Como es lógico, este objeto ha de ser del tipo
de retorno del método en que aparece el return o de
alguno compatible con él, por lo que esta instrucción
sólo podrá incluirse en métodos cuyo tipo de
retorno no sea void, o en los bloques get de las
propiedades o indizadores. De hecho, es obligatorio que todo
método con tipo de retorno termine por un return.
Los métodos que devuelvan void pueden tener un
return con una sintaxis espacial en la que no se indica
ningún valor a devolver sino que simplemente se usa
return para indicar que se desea terminar la
ejecución del método:
return;
Nuevamente, como con el resto de instrucciones de salto hasta ahora
vistas, si se incluyese un return dentro de un bloque
try con cláusula finally, antes de devolverse
el objeto especificado se ejecutarían las instrucciones de
la cláusula finally. Si hubiesen varios bloques
finally anidados, las instrucciones de cada uno es
ejecutarían de manera ordenada (o sea, del más
interno al más externo) Ahora bien, lo que no es posible es
incluir un return dentro de una cláusula
finally.
Instrucción
goto
La instrucción goto permite pasar a ejecutar
el código a partir de una instrucción cuya etiqueta
se indica en el goto. La sintaxis de uso de esta
instrucción es:
goto <etiqueta>;
Como en la mayoría de los lenguajes, goto es una
instrucción maldita cuyo uso no se recomienda porque
dificulta innecesariamente la legibilidad del código y suele
ser fácil simularla usando instrucciones iterativas y
selectivas con las condiciones apropiadas. Sin embargo, en C# se
incluye porque puede ser eficiente usarla si se anidan muchas
instrucciones y para reducir sus efectos negativos se le han
impuesto unas restricciones:
Sólo se pueden etiquetar instrucciones, y no a directivas
using, directivas de preprocesado, definiciones de miembros,
de tipos o de espacios de nombres.
-
La etiqueta indicada no pueda pertenecer a un bloque de
instrucciones anidado dentro del bloque desde el que se usa el
goto ni que etiquete a instrucciones de otro método
diferente a aquél en el cual se encuentra el goto que
la referencia.
-
Para etiquetar una instrucción de modo que pueda ser destino
de un salto con goto basta precederla del nombre con el que se la
quiera etiquetar seguido de dos puntos (:) Por ejemplo, el
siguiente código demuestra cómo usar goto y
definir una etiqueta:
using System;
class HolaMundoGoto
{
public static void Main(string[] args)
{
for (int i=0; i<args.Length; i++)
{
if (args[i] != "salir")
Console.WriteLine(args[i]);
else
goto fin:
}
fin: ;
}
}
Este programa de ejemplo lo que hace es mostrar por pantalla todos
los argumentos que se le pasen como parámetros, aunque si
alguno fuese salir entonces se dejaría de mostrar
argumentos y se aborta la ejecución de la aplicación.
Véase además que este ejemplo pone de manifiesto una
de las utilidades de la instrucción nula, ya que si no
se hubiese escrito tras la etiqueta fin el programa no
compilaría en tanto que toda etiqueta ha de preceder a
alguna instrucción (aunque sea la instrucción nula)
Nótese que al fin y al cabo los usos de goto dentro
de instrucciones switch que se vieron al estudiar dicha
instrucción no son más que variantes del uso general
de goto, ya que default: no es más que una
etiqueta y case <valor>: puede verse como una
etiqueta un tanto especial cuyo nombre es case seguido de
espacios en blanco y un valor. En ambos casos, la etiqueta indicada
ha de pertenecer al mismo switch que el goto usado y no vale
que éste no la contenga pero la contenga algún
switch que contenga al switch del goto.
El uso de goto dentro de sentencias try, tiene las
mismas restricciones que break, continue y
return: antes de salir con un goto de un try
se ejecutará siempre su bloque finally y no es
posible forzar a saltar fuera de un finally.
Instrucción
throw
La instrucción throw ya se ha visto que se usa para
lanzar excepciones de este modo:
throw <objetoExcepciónALanzar>;
En caso de que no se indique ningún
<objetoExcepciónALanzar> se relanzará el que se
estuviese tratando en ese moment, aunque esto sólo es
posible si el throw se ha escrito dentro del código
de tratamiento asociado a alguna cláusula catch.
Como ya se ha explicado a fondo esta instrucción en este
mismo tema, para más información sobre basta
remitirse al epígrafe Excepciones de este tema.
Otras instrucciones
Las instrucciones vistas hasta ahora son comunes a muchos lenguajes
de programación. Sin embargo, en C# también se ha
incluido un buen número de nuevas instrucciones propias de
este lenguaje. Estas instrucciones se describen en los siguientes
apartados:
Instrucciones checked y
unchecked
Las instrucciones checked y unchecked permiten controlar la forma
en que tratarán los desbordamientos que ocurran durante la
realización de operaciones aritméticas con tipos
básico enteros. Funcionan de forma similar a los operadores
checked y unchecked ya vistos en el Tema 4:
Aspectos léxicos, aunque a diferencia de éstos
son aplicables a bloques enteros de instrucciones y no a una
única expresión. Así, la instrucción
checked se usa de este modo:
checked
<instrucciones>
Todo desbordamiento que se produzca al realizar operaciones
aritméticas con enteros en <instrucciones>
provocará que se lance una excepción
System.OverflowException. Por su parte, la
instrucción unchecked se usa así:
unchecked
<instrucciones>
En este caso, todo desbordamiento que se produzca al realizar
operaciones aritméticas con tipos básicos enteros en
<instrucciones> será ignorado y lo que se hará
será tomar el valor resultante de quedarse con los bits
menos significativos necesarios.
Por defecto, en ausencia de estas instrucciones las expresiones
constantes se evalúan como si se incluyesen dentro de una
instrucción checked y las que no constantes como si
se incluyesen dentro de una instrucción unchecked.
Sin embargo, a través de la opción /checked
del compilador es posible tanto hacer que por defecto se comprueben
los desbordamiento en todos los casos para así siempre poder
detectarlos y tratarlos
Desde Visual Studio.NET, la forma de controlar el tipo de
comprobaciones que por defecto se harán es a través
de View -> Propety Pages -> Configuration Settings -> Build ->
Check for overflow underflow.
El siguiente código muestra un ejemplo de cómo usar
ambas instrucciones:
using System;
class Unchecked
{
static short x = 32767; // Valor maximo del tipo short
public static void Main()
{
unchecked
{
Console.WriteLine((short) (x+1)); // (1)
Console.WriteLine((short) 32768); // (2)
}
}
}
En un principio este código compilaría, pero los
desbordamientos producidos por el hecho de que 32768 no es un valor
que se pueda representar con un short (16 bits con signo)
provocaría que apareciese por pantalla dicho valor truncado,
mostrándose:
-32768
-32678
Sin embargo, si sustituyésemos la instrucción
unchecked por checked, el código anterior ni
siquiera compilaría ya que el compilador detectaría
que se va a producir un desbordamiento en (2) debido a que 32768 es
constante y no representable con un short.
Si eliminamos la instrucción (2) el código
compilaría ya que (x+1) no es una expresión constante
y por tanto el compilador no podría detectar desbordamiento
al compilar. Sin embargo, cuando se ejecutase la aplicación
se lanzaría una System.OverflowException.
Instrucción
lock
La instrucción lock es útil en
aplicaciones concurrentes donde múltiples hilos pueden estar
accediendo simultáneamente a un mismo recurso, ya que lo que
hace es garantizar que un hilo no pueda acceder a un recurso
mientras otro también lo esté haciendo. Su sintaxis
es la siguiente:
lock (<objeto>)
<instrucciones>
Su significado es el siguiente: ningún hilo puede ejecutar
las <instrucciones> del bloque indicado si otro las
está ejecutando, y si alguno lo intenta se quedará
esperando hasta que acabe el primero. Esto también afecta a
bloques de <instrucciones> de cualquier otro lock cuyo
<objeto> sea el mismo. Este <objeto> ha de ser de
algún tipo referencia.
En realidad, la instrucción anterior es equivalente a hacer:
System.Threading.Monitor.Enter(<objeto>);
try
<instrucciones>
finally
{
System.Threading.Monitor.Exit(<objeto>);
}
Sin embargo, usar lock tiene dos ventajas: es más
compacto y eficiente (<objeto> sólo se
evalúa una vez)
Una buena forma de garantizar la exclusión mutua durante la
ejecución de un método de un cierto objeto es
usando this como <objeto>. En el caso de que se tratase
de un método de tipo, en tanto que this no tiene
sentido dentro de estos métodos estáticos una buena
alternativa sería usar el objeto System.Type que
representase a ese tipo. Por ejemplo:
class C
{
public static void F()
{
lock(typeof(C))
{
// ... Código al que se accede exclusivamente
}
}
}
Instrucción
using
La instrucción using facilita el trabajo con
objetos que tengan que ejecutar alguna tarea de limpieza o
liberación de recursos una vez que termine de ser
útiles. Aunque para estos menesteres ya están los
destructores, dado su carácter indeterminista puede que en
determinadas ocasiones no sea conveniente confiar en ellos para
realizar este tipo de tareas. La sintaxis de uso de esta
instrucción es la siguiente:
using (<tipo> <declaraciones>)
<instrucciones>
En <declaraciones> se puede indicar uno o varios objetos de
tipo <tipo> separados por comas. Estos objetos serán
de sólo lectura y sólo serán accesibles desde
<instrucciones>. Además, han de implementar la
interfaz System.IDisposable definida como sigue:
interface IDisposable
{
void Dispose()
}
En la implementación de Dispose() se
escribiría el código de limpieza necesario, pues el
significado de using consiste en que al acabar la
ejecución de <instrucciones>, se llama
automáticamente al método Dispose() de los
objetos definidos en <declaraciones>
Hay que tener en cuenta que la llamada a Dispose() se hace sea cual
sea la razón de que se deje de ejecutar las
<instrucciones> Es decir, tanto si se ha producido una
excepción como si se ha acabado su ejecución
normalmente o con una instrucción de salto, Dispose()
es siempre llamado. En realidad una instrucción using
como:
using (R1 r1 = new R1())
{
r1.F();
}
Es tratada por el compilador como:
{
R1 r1 = new R1()
try
{
r1.F();
}
finally
{
if (r1!=null)
((IDisposable) r1).Dispose();
}
}
Si se declarasen varios objetos en <declaraciones>, a
Dispose() se le llamaría en el orden inverso a como
fueron declarados. Lo mismo ocurre si se anidasen varias
instrucciones using: primero se llamaría al
Dispose() de las variables declaradas en los using
internos y luego a las de los externos. Así, estas dos
instrucciones son equivalentes:
using (Recurso obj = new Recurso(), obj2= new Recurso())
{
r1.F();
r2.F();
}
using (Recurso obj = new Recurso())
{
using (Recurso obj2= new Recurso())
{
r1.F();
r2.F();
}
}
El siguiente ejemplo resume cómo funciona la sentencia
using:
using System;
class A:IDisposable
{
public void Dispose()
{
Console.WriteLine("Llamado a Dispose() de {0}", Nombre);
}
public A(string nombre)
{
Nombre = nombre;
}
string Nombre;
}
class Using
{
public static void Main()
{
A objk = new A("objk");
using (A obj1 = new A("obj1"), obj2 = new A("objy"))
{
Console.WriteLine("Dentro del using");
}
Console.WriteLine("Fuera del using");
}
}
La salida por pantalla resultante de ejecutar este ejemplo
será:
Dentro del using
Llamando a Dispose() de objy
Llamando a Dispose() de obj1
Fuera del using
Como se deduce de los mensajes de salida obtenidos, justo antes de
salirse del using se llama a los métodos
Dispose() de los objetos declarados en la sección
<declaraciones> de dicha instrucción y en el
mismo orden en que fueron declarados.
Instrucción
fixed
La instrucción fixed se utiliza para fijar
objetos en memoria de modo que el recolector de basura no pueda
moverlos durante la ejecución de un cierto bloque de
instrucciones.
Esta instrucción sólo tiene sentido dentro de
regiones de código inseguro, concepto que se trata en el
Tema 18: Código inseguro, por lo que será
allí es donde se explique a fondo cómo utilizarla.
Aquí sólo diremos que su sintaxis de uso es:
fixed(<tipoPunteros> <declaracionesPunterosAFijar>)
<instrucciones >