Zona HTML Zona Java Zona PHP Zona ASP Zona Bases de datos
Inicio > Tutoriales > Lenguajes orientados a objeto > C# > El lenguaje de programación C#
-Tutoriales

El lenguaje de programación C#


Tema 7: Variables y tipos de datos


(C) 2001 José Antonio González Seco

. Definición de variables

Una variable puede verse simplemente como un almacén de objetos de un determinado tipo al que se le da un cierto nombre. Por tanto, para definir una variable sólo hay que decir cuál será el nombre que se le dará y cuál será el tipo de datos que podrá almacenar, lo que se hace con la la siguiente sintaxis:

<tipoVariable> <nombreVariable>;

Una variable puede ser definida dentro de una definición de clase, en cuyo caso se correspondería con el tipo de miembro que hasta ahora hemos denominado campo. También puede definirse como un variable local a un método, que es una variable definida dentro del código del método a la que sólo puede accederse desde dentro de dicho código. Otra posibilidad es definirla como parámetro de un método, que son variables que almacenan los valores de llamada al método y que, al igual que las variables locales, sólo puede ser accedidas desde código ubicado dentro del método. El siguiente ejemplo muestra como definir variables de todos estos casos:

class A
{
  int x, z;
  int y;

  void F(string a, string b)
  {
    Persona p;
  }
}

En este ejemplo las variables x, z e y son campos de tipo int, mientras que p es una variable local de tipo Persona y a y b son parámetros de tipo string. Como se muestra en el ejemplo, si un método toma varios parámetros las definiciones de éstos se separan mediante comas (carácter ,), y si queremos definir varios campos o variables locales (no válido para parámetros) de un mismo tipo podemos incluirlos en una misma definición incluyendo en <nombreVariable> sus nombres separados por comas.

Con la sintaxis de definición de variables anteriormente dada simplemente definimos variables pero no almacenamos ningún objeto inicial en ellas. El compilador dará un valor por defecto a los campos para los que no se indique explícitamente ningún valor según se explica en el siguiente apartado. Sin embargo, a la variables locales no les da ningún valor inicial, pero detecta cualquier intento de leerlas antes de darles valor y produce erores de compilación en esos casos.

Ya hemos visto que para crear objetos se utiliza el operador new. Por tanto, una forma de asignar un valor a la variable p del ejemplo anterior sería así:

Persona p;
p = new Persona("José", 22, "76543876-A");

Sin embargo, C# también proporciona una sintaxis más sencilla con la que podremos asignar un objeto a una variable en el mismo momento se define. Para ello se la ha de definir usando esta otra notación:

<tipoVariable> <nombreVariable> = <valorInicial>;

Asi por ejemplo, la anterior asignación de valor a la variable p podría reescribirse de esta otra forma más compacta:

Persona p  = new Persona("José", 22, "76543876-A");

La especificación de un valor inicial también combinarse con la definición de múltiples variables separadas por comas en una misma línea. Por ejemplo, las siguientes definiciones son válidas:

Persona p1 = new Persona("José", 22, "76543876-A"),
  p2 = new Persona("Juan", 21,"87654212-S");

Y son tratadas por el compilador de forma completamente equivalentes a haberlas declarado como:

Persona p1 = new Persona("José", 22, "76543876-A");
Persona p2 = new Persona("Juan", 21, "87654212-S");

. Tipos de datos básicos

Los tipos de datos básicos son ciertos tipos de datos tan comúnmente utilizados en la escritura de aplicaciones que en C# se ha incluido una sintaxis especial para tratarlos. Por ejemplo, para representar números enteros de 32 bits con signo se utiliza el tipo de dato System.Int32 definido en la BCL, aunque 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;

Como se ve, no se utiliza el operador new para crear un objeto 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 todos estos:

Tipo Descripción Bits Rango de valores Alias
SByte Bytes con signo 8 -128 - 127 sbyte
Byte Bytes sin signo 8 0 - 255 byte
Int16 Enteros cortos con signo 16 [-32.768, 32.767] short
UInt16 Enteros cortos sin signo 16 [0, 65.535] ushort
Int32 Enteros normales 32 [-2.147.483.648, 2.147.483.647] int
UInt32 Enteros normales sin signo 32 [0, 4.294.967.295] uint
Int64 Enteros largos 64 [-9.223.372.036.854.775.808, 9.223.372.036.854.775.807] long
UInt64 Enteros largos sin signo 64 [0-18.446.744.073.709.551.615] ulong
Single Reales con 7 dígitos de precisión 32 [1,5×10-45 - 3,4×1038] float
Double Reales de 15-16 dígitos de precisión 64 [5,0×10-324 - 1,7×10308] double
Decimal Reales de 28-29 dígitos de precisión 128 [1,0×10-28 - 7,9×1028] decimal
Boolean Valores lógicos 32 true, false bool
Char Caracteres Unicode 16 ['\u0000', '\uFFFF'] char
String Cadenas de caracteres Variable El permitido por la memoria string
Object Cualquier objeto Variable Cualquier objeto object

Tabla 5: Tipos de datos básicos

Pese a su sintaxis especial, en C# los tipos básicos son tipos del mismo nivel que cualquier otro tipo del lenguaje. Es decir, heredan de System.Object y pueden ser tratados como objetos de dicha clase por cualquier método que espere un System.Object, lo que es muy útil para el diseño de rutinas genéricas que admitan parámetros de cualquier tipo y es una ventaja importante de C# frente a lenguajes similares como Java donde los tipos básicos no son considerados objetos.

El valor que por defecto se da a los campos de tipos básicos consiste en poner a cero todo el área de memoria que ocupen. Esto se traduce en que los campos de tipos básicos numéricos se inicializan por defecto con el valor 0, los de tipo bool lo hacen con false, los de tipo char con '\u0000', y los de tipo string y object con null.

Ahora que sabemos cuáles son los tipos básicos, es el momento de comentar cuáles son los sufijos que admiten los literales numéricos para indicar al compilador cuál es el tipo que se ha de considerar que tiene. Por ejemplo, si tenemos en una clase los métodos:

public static void F(int x)
{...}
public static void F(long x)
{...}

Ante una llamada como F(100), ¿a cuál de los métodos se llamara? Pues bien, en principio se considera que el tipo de un literal entero es el correspondiente al primero de estos tipos básicos que permitan almacenarlo: int, uint, long, ulong, por lo que en el caso anterior se llamaría al primer F() Para llamar al otro podría añadirse el sufijo L al literal y hacer la llamada con F(100L) En la Tabla 6 se resumen los posibles sufijos válidos:

Sufijo Tipo del literal entero
ninguno Primero de: int, uint, long,ulong
L ó l Primero de: long, ulong
U ó u Primero de: int, uint
UL, Ul, uL, ul, LU, Lu, lU ó lu ulong

Tabla 6: Sufijos de literales enteros

Por su parte, en la Tabla 7 se indican los sufijos que admiten los literales reales son:

Sufijo Tipo del literal real
F ó f float
ninguno,D ód double
M ó m decimal

Tabla 7: Sufijos de literales reales

. Tablas

. Tablas unidimensionales

Una tabla unidimensional es un tipo especial de variable que es capaz de almacenar en su interior y de manera ordenada uno o varios datos de un determinado tipo. Para declarar variables de este tipo especial se usa la siguiente sintaxis:

<tipoDatos>[] <nombreTabla>;

Por ejemplo, una tabla que pueda almacenar objetos de tipo int se declara así:

int[] tabla;

Con esto la tabla creada no almacenaría ningún objeto, sino que valdría null. Si se desea que verdaderamente almacene objetos hay que indicar cuál es el número de objetos que podrá almacenar, lo que puede hacerse usando la siguiente sintaxis al declararla:

<tipoDatos>[] <nombreTabla> = new <tipoDatos>[<númeroDatos>];

Por ejemplo, una tabla que pueda almacenar 100 objetos de tipo int se declara así:

int[] tabla = new int[100];

Aunque también sería posible definir el tamaño de la tabla de forma separada a su declaración de este modo:

int[] tabla;
tabla = new int[100];

Con esta última sintaxis es posible cambiar dinámicamente el número de elementos de una variable tabla sin más que irle asignando nuevas tablas. Ello no significa que una tabla se pueda redimensionar conservando los elementos que tuviese antes del cambio de tamaño, sino que ocurre todo lo contrario: cuando a una variable tabla se le asigna una tabla de otro tamaño, sus elementos antiguos son sobreescritos por los nuevos.

Si se crea una tabla con la sintaxis hasta ahora explicada todos sus elementos tendrían el valor por defecto de su tipo de dato. Si queremos darles otros valores al declarar la tabla, hemos de indicarlos entre llaves usando esta sintaxis:

<tipoDatos>[] <nombreTabla> = new <tipoDatos>[] {<valores>};

Ha de especificarse tantos <valores> como número de elementos se desee que tenga la tabla, y si son más de uno se han de separar entre sí mediante comas (,) Nótese que ahora no es necesario indicar el número de elementos de la tabla (aunque puede hacerse) si se desea), pues el compilador puede deducirlo del número de valores especificados. Por ejemplo, para declarar una tabla de cuatro elementos de tipo int con valores 5,1,4,0 se podría hacer lo siguiente:

int[] tabla = new int[] {5,1,4,0};

Incluso se puede compactar aún más la sintaxis declarando la tabla así:

int[] tabla = {5,1,4,0};

También podemos crear tablas cuyo tamaño se pueda establecer dinámicamente a partir del valor de cualquier expresión que produzca un valor de tipo entero. Por ejemplo, para crear una tabla cuyo tamaño sea el valor indicado por una variable de tipo int (luego su valor será de tipo entero) se haría:

int i = 5;
...
int[] tablaDinámica =  new int[i];

A la hora de acceder a los elementos almacenados en una tabla basta indicar entre corchetes, y a continuación de la referencia a la misma, la posición que ocupe en la tabla el elemento al que acceder. Cuando se haga hay que tener en cuenta que en C# las tablas se indexan desde 0, lo que significa que el primer elemento de la tabla ocupará su posición 0, el segundo ocupará la posición 1, y así sucesivamente para el resto de elementos. Por ejemplo, aunque es más ineficiente, la tabla declarada en el último fragmento de código de ejemplo también podría haberse definido así:

int[] tabla = new int[4];

tabla[0] = 5;
tabla[1]++;  // Por defecto se inicializó a 0, luego
             // ahora el valor de tabla[1] pasa a ser 1
tabla[2] = tabla[0] - tabla[1];  // tabla[2] pasa a 
                                 // valer 4, pues 5-4 = 1
// El contenido de la tabla será {5,1,4,0}, pues
// tabla[3] se inicializó por defecto a 0.

Hay que tener cuidado a la hora de acceder a los elementos de una tabla ya que si se especifica una posición superior al número de elementos que pueda almacenar la tabla se producirá una excepción de tipo System.OutOfBoundsException. En el Tema 16: Instrucciones se explica qué son las excepciones, pero por ahora basta considerar que son objetos que informan de situaciones excepcionales (generalmente errores) producidas durante la ejecución de una aplicación. Para evitar este tipo de excepciones puede consultar el valor del campo de sólo lectura Length que está asociado a toda tabla y contiene el número de elementos de la misma. Por ejemplo, para asignar un 7 al último elemento de la tabla anterior se haría:

tabla[tabla.Length - 1] = 7;  // Se resta 1 porque tabla.Length
                              // devuelve 4 pero el último elemento
                              // de la tabla es tabla[3]

. Tablas dentadas

Una tabla dentada no es más que una tabla cuyos elementos son a su vez tablas, pudiéndose así anidar cualquier número de tablas. Para declarar tablas de este tipo se usa una sintaxis muy similar a la explicada para las tablas unidimensionales solo que ahora se indican tantos corchetes como nivel de anidación se desee. Por ejemplo, para crear una tabla de tablas de elementos de tipo int formada por dos elementos, uno de los cuales fuese una tabla de elementos de tipo int formada por los elementos de valores 1,2 y el otro fuese una tabla de elementos de tipo int y valores 3,4,5, se puede hacer:

int[][] tablaDentada = new int[2][] {new int[] {1,2}, new int[] {3,4,5}};

Como se indica explícitamente cuáles son los elementos de la tabla declarada no hace falta indicar el tamaño de la tabla, por lo que la declaración anterior es equivalente a:

int[][] tablaDentada = new int[][] {new int[] {1,2}, new int[] {3,4,5}};

Es más, igual que como se vió con las tablas unidimensionales también es válido hacer:

int[][] tablaDentada = {new int[] {1,2}, new int[] {3,4,5}};

Si no quisiésemos indicar cuáles son los elementos de las tablas componentes, entonces tendríamos que indicar al menos cuál es el número de elementos que podrán almacenar (se inicializarán con valores por defecto) quedando:

int[][] tablaDentada = {new int[2], new int[3]};

Si no queremos crear las tablas componentes en el momento de crear la tabla dentada, entonces tendremos que indicar por lo menos cuál es el número de tablas componentes posibles (cada una valdría null), con lo que quedaría:

int[][] tablaDentada = new int[2][];

Es importante señalar que no es posible especificar todas las dimensiones de una tabla dentada en su definición si no se indica explícitamente el valor inicial de éstas entre llaves. Es decir, esta declaración es incorrecta:

int[][] tablaDentada = new int[2][5];

Esto se debe a que el tamaño de cada tabla componente puede ser distinto y con la sintaxis anterior no se puede decir cuál es el tamaño de cada una. Una opción hubiese sido considerar que es 5 para todas como se hace en Java, pero ello no se ha implementado en C# y habría que declarar la tabla de, por ejemplo, esta manera:

int[][] tablaDentada = {new int[5], new int[5]);

Finalmente, si sólo queremos declarar una variable tabla dentada pero no queremos indicar su número de elementos, (luego la variable valdría null), entonces basta poner:

int[][] tablaDentada;

Hay que precisar que aunque en los ejemplos hasta ahora presentes se han escrito ejemplos basados en tablas dentadas de sólo dos niveles de anidación, también es posible crear tablas dentadas de cualquier número de niveles de anidación. Por ejemplo, para una tabla de tablas de tablas de enteros de 2 elementos en la que el primero fuese una tabla dentada formada por dos tablas de 5 enteros y el segundo elemento fuese una tabla dentada formada por una tabla de 4 enteros y otra de 3 se podría definir así:

int[][][] tablaDentada = new int[][][]
                         { new int[][] {new int[5], new int[5]},
                           new int[][] {new int[4], new int[3]}};

A la hora de acceder a los elementos de una tabla dentada lo único que hay que hacer es indicar entre corchetes cuál es el elemento exacto de las tablas componentes al que se desea acceder, indicándose un elemento de cada nivel de anidación entre unos corchetes diferentes pero colocándose todas las parejas de corchetes juntas y ordenadas de la tabla más externa a la más interna. Por ejemplo, para asignar el valor 10 al elemento cuarto de la tabla que es elemento primero de la tabla que es elemento segundo de la tabla dentada declarada en último lugar se haría:

tablaDentada[1][0][3] = 10;

. Tablas multidimensionales

Una tabla multidimensional es una tabla cuyos elementos se encuentran organizando una estructura de varias dimensiones. Para definir este tipo de tablas se usa una sintaxis similar a la usada para declarar tablas unidimensionales pero separando las diferentes dimensiones mediante comas (,) Por ejemplo, una tabla multidimensional de elementos de tipo int que conste de 12 elementos puede tener sus elementos distribuidos en dos dimensiones formando una estructura 3x4 similar a una matriz de la forma:

1   2   3   4
5   6   7   8
9 10 11 12

Esta tabla se podría declarar así:

int[,] tablaMultidimensional = new int[3,4]          

En realidad no es necesario indicar el número de elementos de cada dimensión de la tabla ya que pueden deducirse de los valores explícitamente indicados entre llaves, por lo que la definición anterior es similar a esta:

int[,] tablaMultidimensional = new int[,]          

Incluso puede reducirse aún más la sintaxis necesaria quedando tan sólo:

int[,] tablaMultidimensional =           

Si no queremos indicar explícitamente los elementos de la tabla al declararla, podemos obviarlos pero aún así indicar el tamaño de cada dimensión de la tabla (a los elementos se les daría el valor por defecto de su tipo de dato) así:

int[,] tablaMultidimensional = new int[3,4];

También podemos no especificar ni siquiera el número de elementos de la tabla de esta forma (tablaMultidimensional contendría ahora null):

int[,] tablaMultidimensional;

Aunque los ejemplos de tablas multidimensionales hasta ahora mostrados son de tablas de dos dimensiones, en general también es posible crear tablas de cualquier número de dimensiones. Por ejemplo, una tabla que almacene 24 elementos de tipo int y valor 0 en una estructura tridimensional 3x4x2 se declararía así:

int[,,] tablaMultidimensional = new int[3,4,2];

El acceso a los elementos de una tabla multidimensional es muy sencillo: sólo hay que indicar los índices de la posición que ocupe en la estructura multidimensional el elemento al que se desee acceder. Por ejemplo, para incrementar en una unidad el elemento que ocupe la posición (1,3,2) de la tabla anterior se haría (se indiza desde 0):

tablaMultidimensional[0,2,1]++;

Nótese que tanto las tablas dentadas como las tablas multidimensionales pueden ser utilizadas tanto para representar estructuras matriciales como para, en general, representar cualquier estructura de varias dimensiones. La diferencia entre ambas son:

  • Como las tablas dentadas son tablas de tablas, cada uno de sus elementos puede ser una tabla de un tamaño diferente. Así, con las tablas dentadas podemos representar matrices en las que cada columna tenga un tamaño distinto (por el aspecto "aserrado" de este tipo de matrices es por lo que se les llama tablas dentadas), mientras que usando tablas multidimensionales sólo es posible crear matrices rectangulares o cuadradas. Las estructuras aserradas pueden simularse usando matrices multidimensionales con todas sus columnas del tamaño de la columna más grande necesaria, aunque ello implica desperdiciar mucha memoria sobre todo si los tamaños de cada columna son muy diferentes y la tabla es grande De todos modos, las estructuras más comunes que se usan en la mayoría de aplicaciones suelen ser rectangulares o cuadradas.
  • Los tiempos que se tardan en crear y destruir tablas dentadas son superiores a los que se tardan en crear y destruir tablas multidimensionales. Esto se debe a que las primeras son tablas de tablas mientras que las segundas son una única tabla, Por ejemplo, para crear una tabla dentada [100][100] hay que crear 101 tablas (la tabla dentada más las 100 tablas que contiene), mientras que para crear una crear una tabla bidimensional [100,100] hay que crear una única tabla.
  • Las tablas dentadas no forman parte del CLS, por lo que no todos los lenguajes gestionados los tienen porqué admitir. Por ejemplo Visual Basic.NET no las admite, por lo que al usarlas en miembros públicos equivale a perder interoperabilidad con estos lenguajes.

. Tablas mixtas

Una tabla mixta es simplemente una tabla formada por tablas multidimensionales y dentadas combinadas entre sí de cualquier manera. Para declarar una tabla de este tipo basta con tan solo combinar las notaciones ya vistas para las multidimensionales y dentadas. Por ejemplo, para declarar una tabla de tablas multidimensionales cuyos elementos sean tablas unidimensionales de enteros se haría lo siguiente:

int[][,][] tablaMixta;

. Covarianza de tablas

La covarianza de tablas es el resultado de llevar el polimorfismo al mundo de las tablas. Es decir, es la capacidad de toda tabla de poder almacenar elementos de clases hijas de la clase de elementos que pueda almacenar. Por ejemplo, en tanto que todas clases son hijas de System.Object, la siguiente asignación es válida:

string[] tablaCadenas = {"Manolo", "Paco", "Pepe"};
object[] tablaObjetos = tablaCadenas;

Hay que tener en cuenta que la covarianza de tablas sólo se aplica a objetos de tipos referencia y no a objetos de tipos valor Por ejemplo, la siguiente asignación no sería válida en tanto que int es un tipo por valor:

int[] tablaEnteros = {1, 2, 3};
object[] tablaObjetos = tablaEnteros;

. La clase System.Array

En realidad, todas las tablas que definamos, sea cual sea el tipo de elementos que contengan, son objetos que derivan de System.Array. Es decir, van a disponer de todos los miembros que se han definido para esta clase, entre los que son destacables:

  • Length: Campo de sólo lectura que informa del número total de elementos que contiene la tabla. Si la tabla tiene más de una dimensión o nivel de anidación indica el número de elementos de todas sus dimensiones y niveles. Por ejemplo:
    int[] tabla = {1,2,3,4};
    int[][] tabla2 = {new int[] {1,2}, new int[] {3,4,5}};
    int[,] tabla3 = 
    Console.WriteLine(tabla.Length);   //Imprime 4
    Console.WriteLine(tabla2.Length);  //Imprime 5
    Console.WriteLine(tabla3.Length);  //Imprime 6
  • Rank: Campo de sólo lectura que almacena el número de dimensiones de la tabla. Obviamente si la tabla no es multidimensional valdrá 1. Por ejemplo:
    int[] tabla = {1,2,3,4};
    int[][] tabla2 = {new int[] {1,2}, new int[] {3,4,5}};
    int[,] tabla3 = 
    Console.WriteLine(tabla.Rank);   //Imprime 1
    Console.WriteLine(tabla2.Rank);  //Imprime 1
    Console.WriteLine(tabla3.Rank);  //Imprime 2
  • int GetLength(int dimensión): Método que devuelve el número de elementos de la dimensión especificada. Las dimensiones se indican empezando a contar desde cero, por lo que si quiere obtenerse el número de elementos de la primera dimensión habrá que usar GetLength(0), si se quiere obtener los de la segunda habrá que usar GetLength(1), etc. Por ejemplo:
    int[,] tabla = 
    Console.WriteLine(tabla.GetLength(0));    // Imprime 2
    Console.WriteLine(tabla.GetLength(1));    // Imprime 4
  • void CopyTo(Array destino, int posición): Copia todos los elementos de la tabla sobre la que es aplica en la que se le pasa como primer parámetro a partir de la posición de la misma indicada como segundo parámetro. Por ejemplo:
    int[] tabla1 = {1,2,3,4};
    int[] tabla2 = {5,6,7,8};
    
    tabla1.CopyTo(tabla2,0);  // A partir de ahora, ambas tablas contienen {1,2,3,4}

    Ambas tablas han de ser unidimensionales. Por otro lado, y como es obvio, la tabla de destino ha de ser de un tipo que pueda almacenar los objetos de la tabla fuente, el índice especificado ha de ser válido (mayor o igual que cero y menor que el tamaño de la tabla de destino) y no ha de valer null ninguna. Si no fuese así, saltarían excepciones de diversos tipos informando del error cometido (en la documentación del SDK puede ver cuáles son en concreto).

Aparte de los miembros aquí señalados, de System.Array cuenta con muchos más que permiten realizar tareas tan frecuentes como búsquedas de elementos, ordenaciones, etc.

. Cadenas de texto

Una cadena de texto no es más que una secuencia de caracteres Unicode. En C# se representan mediante objetos del tipo tipo de dato llamado string, que no es más que un alias del tipo System.String incluido en la BCL.

Las cadenas de texto suelen crearse a partir literales de cadena o de otras cadenas previamente creadas. Ejemplos de ambos casos se muestran a continuación:

string cadena1 = "José Antonio";
string cadena2 = cadena;

En el primer caso se ha creado un objeto string que representa a la cadena formada por la secuencia de caracteres José Antonio indicada literalmente (nótese que las comillas dobles entre las que se encierran los literales de cadena no forman parte del contenido de la cadena que representan sino que sólo se usan como delimitadores de la misma) En el segundo caso la variable cadena2 creada se genera a partir de la variable cadena1 ya existente, por lo que ambas variables apuntarán al mismo objeto en memoria.

Hay que tener en cuenta que el tipo string es un tipo referencia, por lo que en principio la comparación entre objetos de este tipo debería comparar sus direcciones de memoria como pasa con cualquier tipo referencia. Sin embargo, si ejecutamos el siguiente código veremos que esto no ocurre en el caso de las cadenas:

using System;

public class IgualdadCadenas
{
  public static void Main()
  {
    string cadena1 = "José Antonio";
    string cadena2 = String.Copy(cadena1);

    Console.WriteLine(cadena1==cadena2);
  }
}

El método Copy() de la clase String usado devuelve una copia del objeto que se le pasa como parámetro. Por tanto, al ser objetos diferentes se almacenarán en posiciones distintas de memoria y al compararlos debería devolverse false como pasa con cualquier tipo referencia. Sin embargo, si ejecuta el programa verá que lo que se obtiene es precisamente lo contrario: true. Esto se debe a que para hacer para hacer más intuitivo el trabajo con cadenas, en C# se ha modificado el operador de igualdad para que cuando se aplique entre cadenas se considere que sus operandos son iguales sólo si son lexicográficamente equivalentes y no si referencian al mismo objeto en memoria. Además, esta comparación se hace teniendo en cuenta la capitalización usada, por lo que "Hola"=="HOLA" ó "Hola"=="hola" devolverán false ya que contienen las mismas letras pero con distinta capitalización.

Si se quisiese comparar cadenas por referencia habría que optar por una de estas dos opciones: compararlas con Object.ReferenceEquals() o convertirlas en objects y luego compararlas con == Por ejemplo:

Console.WriteLine(Object.ReferecenceEquals(cadena1, cadena2));
Console.WriteLine( (object) cadena1 == (object) cadena2);

Ahora sí que lo que se comparan son las direcciones de los objetos que representan a las cadenas en memoria, por lo que la salida que se mostrará por pantalla es:

False
False

Hay que señalar una cosa, y es que aunque en principio el siguiente código debería mostrar la misma salida por pantalla que el anterior ya que las cadenas comparadas se deberían corresponder a objetos que aunque sean lexicográficamente equivalentes se almacenan en posiciones diferentes en memoria:

using System;

public class IgualdadCadenas2
{
  public static void Main()
  {
    string cadena1 = "José Antonio";
    string cadena2 = "José Antonio";
    
    Console.WriteLine(Object.ReferenceEquals(cadena1, cadena2));
    Console.WriteLine( ((object) cadena1) == ((object) cadena2));
  }
}

Si lo ejecutamos veremos que la salida obtenida es justamente la contraria:

True
True

Esto se debe a que el compilador ha detectado que ambos literales de cadena son lexicográficamente equivalentes y ha decidido que para ahorra memoria lo mejor es almacenar en memoria una única copia de la cadena que representan y hacer que ambas variables apunten a esa copia común. Esto va a afectar a la forma en que es posible manipular las cadenas como se explicará más adelante.

Al igual que el significado del operador == ha sido especialmente modificado para trabajar con cadenas, lo mismo ocurre con el operador binario +. En este caso, cuando se aplica entre dos cadenas o una cadena y un carácter lo que hace es devolver una nueva cadena con el resultado de concatenar sus operandos. Así por ejemplo, en el siguiente código las dos variables creadas almacenarán la cadena Hola Mundo:

public class Concatenación
{
  public static void Main()
  {
    string cadena = "Hola" + " Mundo";
    string cadena2 = "Hola Mund" + 'o';
  }
}

Por otro lado, el acceso a las cadenas se hace de manera similar a como si de tablas de caracteres se tratase: su "campo" Length almacenará el número de caracteres que la forman y para acceder a sus elementos se utiliza el operador []. Por ejemplo, el siguiente código muestra por pantalla cada carácter de la cadena Hola en una línea diferente:

using System;

public class AccesoCadenas
{
  public static void Main()
  {
    string cadena = "Hola";
    
    Console.WriteLine(cadena[0]);
    Console.WriteLine(cadena[1]);
    Console.WriteLine(cadena[2]);
    Console.WriteLine(cadena[3]);
  }
}

Sin embargo, hay que señalar una diferencia importante respecto a la forma en que se accede a las tablas: las cadenas son inmutables, lo que significa que no es posible modificar los caracteres que las forman. Esto se debe a que el compilador comparte en memoria las referencias a literales de cadena lexicográficamente equivalentes para así ahorrar memoria, y si se permitiese modificarlos los cambios que se hiciesen a través de una variable a una cadena compartida afectarían al resto de variables que la compartan, lo que podría causar errores difíciles de detectar. Por tanto, hacer esto es incorrecto:

string cadena = "Hola";
cadena[0]="A"; //Error: No se pueden modificar las cadenas

Sin embargo, el hecho de que no se puedan modificar las cadenas no significa que no se puedan cambiar los objetos almacenados en las variables de tipo string. Por ejemplo, el siguiente código es válido:

String cad = "Hola";
cad = "Adios";  // Correcto, pues no se modifica la cadena
                // almacenada en cad sino que se hace que cad
                // pase a almacenar otra cadena distinta..
         

Si se desea trabajar con cadenas modificables puede usarse Sytem.Text.StringBuilder, que funciona de manera similar a string pero permite la modificación de sus cadenas en tanto que estas no se comparten en memoria. Para crear objetos de este tipo basta pasar como parámetro de su constructor el objeto string que contiene la cadena a representar mediante un StringBuilder, y para convertir un StringBuilder en String siempre puede usarse su método ToString() heredado de System.Object. Por ejemplo:

using System.Text;
using System;

public class ModificaciónCadenas
{
  public static void Main()
  {
    StringBuilder cadena = new StringBuilder("Pelas");
    String cadenaInmutable;
    
    cadena[0] = 'V';
    Console.WriteLine(cadena); // Muestra Velas     
    cadenaInmutable = cadena.ToString();
    Console.WriteLine(cadenaInmutable); // Muestra Velas              
  }
}

Aparte de los métodos ya vistos, en la clase System.String se definen muchos otros métodos aplicables a cualquier cadena y que permiten manipularla. Los principales son:

  • int IndexOf(string subcadena): Indica cuál es el índice de la primera aparición de la subcadena indicada dentro de la cadena sobre la que se aplica. La búsqueda de dicha subcadena se realiza desde el principio de la cadena, pero es posible indicar en un segundo parámetro opcional de tipo int cuál es el índice de la misma a partir del que se desea empezar a buscar. Del mismo modo, la búsqueda acaba al llegar al final de la cadena sobre la que se busca, pero pasando un tercer parámetro opcional de tipo int es posible indicar algún índice anterior donde terminarla.

    Nótese que es un método muy útil para saber si una cadena contiene o no alguna subcadena determinada, pues sólo si no la encuentra devuelve un -1.

  • int LastIndexOf(string subcadena): Funciona de forma similar a IndexOf() sólo que devuelve la posición de la última aparición de la subcadena buscada en lugar de devolver la de la primera.
  • string Insert(int posición, string subcadena): Devuelve la cadena resultante de insertar la subcadena indicada en la posición especificada de la cadena sobre la que se aplica.
  • string Remove(int posición, int número): Devuelve la cadena resultante de eliminar el número de caracteres indicado que hubiese en la cadena sobre al que se aplica a partir de la posición especificada.
  • string Replace(string aSustituir, string sustituta): Devuelve la cadena resultante de sustituir en la cadena sobre la que se aplica toda aparición de la cadena aSustituir indicada por la cadena sustituta especificada como segundo parámetro.
  • string Substring(int posición, int número): Devuelve la subcadena de la cadena sobre la que se aplica que comienza en la posición indicada y tiene el número de caracteres especificados. Si no se indica dicho número se devuelve la subcadena que va desde la posición indicada hasta el final de la cadena.
  • string ToUpper() y string ToLower(): Devuelven, respectivamente, la cadena que resulte de convertir a mayúsculas o minúsculas la cadena sobre la que se aplican.

Es preciso incidir en que aunque hayan métodos de inserción, reemplazo o eliminación de caracteres que puedan dar la sensación de que es posible modificar el contenido de una cadena, en realidad las cadenas son inmutables y dicho métodos lo que hacen es devolver una nueva cadena con el contenido correspondiente a haber efectuado las operaciones de modifiación solicitadas sobre la cadena a la que se aplican. Por ello, las cadenas sobre las que se aplican quedan intactas como muestra el siguiente ejemplo:

Using System;

public class EjemploInmutabilidad
{
  public static void Main() 
  {
    string cadena1="Hola";
    string cadena2=cadena1.Remove(0,1);
    
    Console.WriteLine(cadena1);
    Console.WriteLine(cadena2);
  }
}

La salida por pantalla de este ejemplo demuestra lo antes dicho, pues es:

Hola
ola

Como se ve, tras el Remove() la cadena1 permance intacta y el contenido de cadena2 es el que debería tener cadena1 si se le hubiese eliminado su primer carácter.

. Constantes

Una constante es una variable cuyo valor puede determinar el compilador durante la compilación y puede aplicar optimizaciones derivadas de ello. Para que esto sea posible se ha de cumplir que el valor de una constante no pueda cambiar durante la ejecución, por lo que el compilador informará con un error de todo intento de modificar el valor inicial de una constante. Las constantes se definen como variables normales pero precediendo el nombre de su tipo del modificador const y dándoles siempre un valor inicial al declararlas. O sea, con esta sintaxis:

const <tipoConstante> <nombreConstante> = <valor>;

Así, ejemplos de definición de constantes es el siguiente:

const int a = 123;
const int b = a + 125;

Dadas estas definiciones de constantes, lo que hará el compilador será sustituir en el código generado todas las referencias a las constantes a y b por los valores 123 y 248 respectivamente, por lo que el código generado será más eficiente ya que no incluirá el acceso y cálculo de los valores de a y b. Nótese que puede hacer esto porque en el código se indica explícitamente cual es el valor que siempre tendrá a y, al ser este un valor fijo, puede deducir cuál será el valor que siempre tendrá b. Para que el compilador pueda hacer estos cálculos se ha de cumplir que el valor que se asigne a las constantes en su declaración sea una expresión constante. Por ejemplo, el siguiente código no es válido en tanto que el valor de x no es constante:

int x = 123;           // x es una variable normal, no una constante
const int y = x +123;  // Error: x no tiene porqué tener valor
                       // constante (aunque aquí lo tenga)

Debido a la necesidad de que el valor dado a una constante sea precisamente constante, no tiene mucho sentido crear constantes de tipos de datos no básicos, pues a no ser que valgan null sus valores no se pueden determinar durante la compilación sino únicamente tras la ejecución de su constructor. La única excepción a esta regla son los tipos enumerados, cuyos valores se pueden determinar al compilar como se explicará cuando los veamos en el Tema 14: Enumeraciones

Todas las constantes son implícitamente estáticas, por lo se considera erróneo incluir el modificador static en su definición al no tener sentido hacerlo. De hecho, para leer su valor desde códigos externos a la definición de la clase donde esté definida la constante, habrá que usar la sintaxis <nombreClase>.<nombreConstante> típica de los campos static.

Por último, hay que tener en cuenta que una variable sólo puede ser definida como constante si se es una variable local o un campo, pero no si es un parámetro.

. Variables de sólo lectura

Dado que hay ciertos casos en los que resulta interesante disponer de la capacidad de sólo lectura que tienen las constantes pero no es posible usarlas debido a las restricciones que hay impuestas sobre su uso, en C# también se da la posibilidad de definir variables que sólo puedan ser leídas. Para ello se usa la siguiente sintaxis:

readonly <tipoConstante> <nombreConstante> = <valor>;

Estas variables superan la mayoría de las limitaciones de las constantes. Por ejemplo:

  • No es obligatorio darles un valor al definirlas, sino que puede dárseles en el constructor. Ahora bien, una vez dado un valor a una variable readonly ya no es posible volverlo a modificar. Si no se le da ningún valor ni en su constructor ni en su definición tomará el valor por defecto correspondiente a su tipo de dato.
  • No tienen porqué almacenar valores constantes, sino que el valor que almacenen puede calcularse durante la ejecución de la aplicación.
  • No tienen porqué definirse como estáticas, aunque si se desea puede hacerse.
  • Su valor se determina durante la ejecución de la aplicación, lo que permite la actualización de códigos cliente sin necesidad de recompilar. Por ejemplo, dado:
namespace Programa1
{
  public class Utilidad
  {
    public static readonly int X = 1;
  }
}
namespace Programa2
{
  class Test
  {
    public static void Main() {
      System.Console.WriteLine(Programa1.Utilidad.X);
} } }

En principio, la ejecución de este programa producirá el valor 1. Sin embargo, si cada espacio de nombres se compilan en módulos de código separados que luego se enlazan dinámicamete y cambiamos el valor de X, sólo tendremos que recompilar el módulo donde esté definido Programa1.Utilidad y Programa2.Test podrá ejecutarse usando el nuevo valor de X sin necesidad de recompilalo.

Sin embargo, pese a las ventajas que las variables de sólo lectura ofrecen respecto a las constantes, tienen dos inconvenientes respecto a éstas: sólo pueden definirse como campos (no como variables locales) y con ellas no es posible realizar las optimizaciones de código comentadas para las constantes.

. Orden de inicialización de variables

Para deducir el orden en que se inicializarán las variables de un tipo de dato basta saber cuál es el momento en que se inicializa cada una y cuando se llama a los constructores:

  • Los campos estáticos sólo se inicializan la primera vez que se accede al tipo al que pertenecen, pero no en sucesivos accesos. Estos accesos pueden ser tanto para crear objetos de dicho tipo como para acceder a sus miembros estáticos. La inicialización se hace de modo que en primer lugar se dé a cada variable el valor por defecto correspondiente a su tipo, luego se dé a cada una el valor inicial especificado al definirlas, y por último se llame al constructor del tipo. Un constructor de tipo es similar a un constructor normal sólo que en su código únicamente puede accederse a miembros static (se verá en el Tema 8: Métodos)
  • Los campos no estáticos se inicializan cada vez que se crea un objeto del tipo de dato al que pertenecen. La inicialización se hace del mismo modo que en el caso de los campos estáticos, y una vez terminada se pasa a ejecutar el código del constructor especificado al crear el objeto. En caso de que la creación del objeto sea el primer acceso que se haga al tipo de dato del mismo, entonces primero se inicializarán los campos estáticos y luego los no estáticos.
  • Los parámetros se inicializan en cada llamada al método al que pertenecen con los valores especificados al llamarlo.
  • Las variables locales se inicializan en cada llamada al método al cual pertenecen pero tras haberse inicializado los parámetros definidos para el mismo. Si no se les da valor inicial no toman ninguno por defecto, considerándose erróneo todo acceso de lectura que se haga a las mismas mientras no se les escriba algún valor.

Hay que tener en cuenta que al definirse campos estáticos pueden hacerse definiciones cíclicas en las que el valor de unos campos dependa del de otros y el valor de los segundos dependa del de los primeros. Por ejemplo:

class ReferenciasCruzadas
{
  static int a = b + 1;
  static int b = a + 1;

  public static void Main()
  {
    System.Console.WriteLine("a = {0}, b = {1}", a, b);
  }
}

Esto sólo es posible hacerlo al definir campos estáticos y no entre campos no estáticas o variables locales, ya que no se puede inicializar campos no estáticos en función del valor de otros miembros no estáticos del mismo objeto porque el objeto aún no estaría incializados, y no se puede inicializar variables locales en función del valor de otras variables locales definidas más adelante porque no se pueden leer variables no inicializadas. Además, aunque las constantes sean implícitamente estáticas tampoco puede hacerse definiciones cíclicas entre constantes.

En primer lugar, hay que señalar que escribir un código como el del ejemplo anterior no es un buen hábito de programación ya que dificulta innecesariamente la legibilidad del programa. Aún así, C# admite este tipo de códigos y para determinar el valor con que se inicializarán basta tener en cuenta que siempre se inicializan primero todos los campos con sus valores por defecto y luego se inicializan aquellos que tengan valores iniciales con dichos valores iniciales y en el mismo orden en que aparezcan en el código fuente. De este modo, la salida del programa de ejemplo anterior será:

a = 1, b = 2

Nótese que lo que se ha hecho es inicializar primero a y b con sus valores por defecto (0 en este caso), luego calcular el valor final de a y luego calcular el valor final de b. Como b vale 0 cuando se calcula el valor final de a, entonces el valor final de a es 1; y como a vale 1 cuando se calcula el valor final de b, entonces el valor final de b es 2.

 
Patrocinados
 

Copyright © 1999-2006 Programación en castellano. Todos los derechos reservados.
Formulario de Contacto - Datos legales - Publicidad

Hospedaje web y servidores dedicados linux por Ferca Network