MÉTODOS

 

Ya dijimos en la introducción a la POO que los métodos son todos aquellos bloques de código que se ocupan de manejar los datos de la clase. Recapitulemos un momento y echemos un nuevo vistazo al ejemplo del coche que pusimos en la introducción. En él teníamos tres métodos: Acelerar, Girar y Frenar, que servían para modificar la velocidad y la dirección de los objetos de la clase coche. Como ves, los métodos sirven para que los objetos puedan ejecutar una serie de acciones. Veamos cómo se define un método en C#:

 

acceso tipo NombreMetodo(TipoArg1 arguento1, TipoArg2 arguento2 ...)

{

    // Aquí se codifica lo que tiene que hacer el método

}

 

Veamos: acceso es el modificador de acceso del método, que puede ser private, protected, internal o public (como las variables). Posteriormente el tipo de retorno, es decir, el tipo de dato que devolverá el método (que puede ser cualquier tipo). Luego el nombre del método (sin espacios en blanco ni cosas raras). Después, entre paréntesis y separados unos de otros por comas, la lista de argumentos que aceptará el método: cada uno de ellos se especificará poniendo primero el tipo y después el nombre del mismo. Por fin, la llave de apertura de bloque seguida del código del método y, para terminarlo, la llave de cierre del bloque.

 

Vamos a ilustrar esto con un ejemplo: vamos a construir una clase Bolígrafo; sus métodos serán, por ejemplo, Pintar y Recargar, que son las operaciones que se suelen efectuar con un bolígrafo. Ambos métodos modificarán la cantidad de tinta del boli, valor que podríamos poner en una propiedad llamada Tinta, por ejemplo. Para aquellos que conozcáis la programación procedimental, un método es como un procedimiento o una función. En determinadas ocasiones necesitaremos pasarle datos a los métodos para que estos puedan hacer su trabajo. Por ejemplo, siguiendo con el bolígrafo, puede que necesitemos decirle al método Pintar la cantidad de tinta que vamos a gastar, igual que hacíamos con el método Acelerar de la clase Coche, que teníamos que decirle cuánto queríamos acelerar. Pues bien, estos datos se llaman argumentos. Vamos a verlo:

 

using System;

 

class Boligrafo

{

    protected int color=0;

    protected byte tinta=100;

 

    public bool Pintar(byte gasto)

    {

        if (gasto>this.tinta) return false;

 

        this.tinta -= gasto;

        Console.WriteLine("Se gastaron {0} unidades de tinta.", gasto);

        return true;

    }

 

    public void Recargar()

    {

        this.tinta=100;

        Console.WriteLine("Bolígrafo recargado");

    }

 

    public int Color

    {

        get

            {

                return this.color;

            }

        set

            {

                this.color = value;

            }

    }

 

    public byte Tinta

    {

        get

        {

            return this.tinta;

        }

    }

}

 

De momento fíjate bien en lo que conoces y en lo que estamos explicando, que son los métodos. Lo demás lo iremos conociendo a su debido tiempo. En este ejemplo tienes los métodos Pintar y Recargar (presta especial atención a la sintaxis). El primero disminuye la cantidad de tinta, y el segundo establece esta cantidad nuevamente a 100, es decir, rellena el bolígrafo de tinta.

 

Los métodos también pueden devolver un valor después de su ejecución si fuera necesario. En este ejemplo, el método Pintar devuelve True si la operación se ha podido efectuar y False si no se ha podido (fíjate en que el tipo de retorno es bool). De este modo, el cliente simplemente debería fijarse en el valor devuelto por el método para saber si todo ha funcionado correctamente, sin tener que comparar los datos de antes con los de después (es decir, sin comprobar si el valor de la propiedad tinta, en este caso, se ha visto modificado). Este método Main que vamos a poner a continuación demostrará el funcionamiento de la clase Bolígrafo:

 

class BoligrafoApp

{

    static void Main()

    {

        // Instanciación del objeto

        Boligrafo boli = new Boligrafo();

        Console.WriteLine("El boli tiene {0} unidades de tinta", boli.Tinta);

 

        Console.WriteLine("boli.Pintar(50) devuelve {0}", boli.Pintar(50));

        Console.WriteLine("Al boli le quedan {0} unidades de tinta", boli.Tinta);

 

        Console.WriteLine("boli.Pintar(60) devuelve {0}", boli.Pintar(60));

        Console.WriteLine("Al boli le quedan {0} unidades de tinta", boli.Tinta);

 

        boli.Recargar();

        Console.WriteLine("Al boli le quedan {0} unidades de tinta", boli.Tinta);

 

        string a = Console.ReadLine();

    }

}

 

Bien, la salida en consola de este programa sería la siguiente:

 

El boli tiene 100 unidades de tinta

Se gastaron 50 unidades de tinta.

boli.Pintar(50) devuelve True

Al boli le quedan 50 unidades de tinta

boli.Pintar(60) devuelve False

Al boli le quedan 50 unidades de tinta

Bolígrafo recargado

Al boli le quedan 100 unidades de tinta

 

Examinemos el código y el resultado un momento. En primer lugar, como ves, instanciamos el objeto boli con el operador new y escribimos la cantidad de tinta del mismo en la consola. Efectivamente, Tinta vale 100 porque la variable protected que almacena este valor (la variable tinta) está inicializada a 100 en la declaración. A continuación, en el método Main, se pretende escribir lo que devuelva el método Pintar. Sin embargo, como ves, antes de eso aparece en la consola otra línea, la que escribe precisamente este método (Pintar). ¿Por qué sale primero esto y después lo que está escrito en el método Main? Pues hombre, para que el método devuelva algo se tiene que haber ejecutado primero. Lógico, ¿no? Bien, como ves, la primera llamada al método Pintar devuelve True porque había tinta suficiente para hacerlo. Después se escribe la tinta que queda y se vuelve a llamar al método Pintar, pero esta vez le pasamos como argumento un número mayor que la tinta que quedaba. Por este motivo, ahora el método pintar devuelve False y no escribe nada en la consola. Posteriormente se ejecuta el método Recargar, que no devuelve nada y escribe "Bolígrafo recargado" en la consola, y, por último, se vuelve a escribir la cantidad de tinta, que vuelve a ser 100. De todo esto podemos extraer dos ideas principales con las que quiero que te quedes de momento: una es que los métodos pueden devolver un valor de cualquier tipo, y la otra es que si un método no devuelve nada hay que declararlo de tipo void.

 

Veamos todo esto con otro ejemplo. Vamos a escribir una clase (muy simplificada, eso sí) que se ocupe de manejar gastos e ingresos, sin intereses ni nada:

 

class Cuentas

{

    protected double saldo=0;

    public double Saldo

    {

        get

        {

            return this.saldo;

        }

    }

      

    public bool NuevoGasto(double cantidad)

    {

        if (cantidad<=0) return false;

 

        this.saldo -= cantidad;

        return true;

    }

 

    public bool NuevoIngreso(double cantidad)

    {

        if (cantidad <=0) return false;

 

        this.saldo += cantidad;

        return true;

    }

}

 

En esta clase hay una variable protected (o sea, que es visible dentro de la clase y dentro de clases derivadas, pero no desde el cliente), una propiedad y dos métodos. Como te dije antes, presta especial atención a lo que conoces y, sobre todo, a los métodos, que es con lo que estamos. Los métodos NuevoIngreso y NuevoGasto se ocupan de modificar el valor de la variable saldo según cuánto se ingrese o se gaste. Ahora bien, si la cantidad que se pretende ingresar es menor o igual que cero, el método no modificará el valor de la variable saldo y devolverá false. Quiero que te fijes de nuevo en cómo se declara un método: en primer lugar el modificador de acceso (que puede ser public, protected, private o internal), después el tipo de dato que retornará, que podrá ser cualquier tipo de dato ( y en caso de que el método no devuelva ningún dato, hay que poner void), después el nombre del método y, por último, la lista de argumentos entre paréntesis. Ya sé que me estoy repitiendo, pero es que esto es muy importante.

 

Sobrecarga de métodos

 

La sobrecarga de métodos consiste en poner varios métodos con el mismo nombre en la misma clase, pero siempre que su lista de argumentos sea distinta. Ojo, repito, siempre que su lista de argumentos sea distinta, es decir, no puede haber dos métodos que se llamen igual con la misma lista de argumentos, aunque devuelvan datos de distinto tipo. El compilador sabría a cuál de todas las sobrecargas nos referimos por los argumentos que se le pasen en la llamada, pero no sería capaz de determinar cuál de ellas debe ejecutar si tienen la misma lista de argumentos. Por ejemplo, no podríamos sobrecargar el método NuevoIngreso de este modo:

 

public int NuevoIngreso(double cantidad) //Error. No se puede sobrecargar así

{...}

 

A pesar de devolver un valor int en lugar de un bool, su lista de argumentos es idéntica, por lo que el compilador avisaría de un error. Sin embargo, podríamos sobrecargalo de estos modos:

 

public bool NuevoIngreso(single cant)

{...}

 

public int NuevoIngreso(double cantidad, double argumento2)

{...}

 

public int NuevoIngreso(single cantidad, double argumento2)

{...}

 

Cada sobrecarga tiene marcado en negrilla el elemento que la hace diferente de las demás. Y así hasta hartarnos de añadir sobrecargas. Hay un detalle que también es importante y que no quiero pasar por alto: lo que diferencia las listas de argumentos de las diferentes sobrecargas no es el nombre de las variables, sino el tipo de cada una de ellas. Por ejemplo, la siguiente sobrecarga tampoco sería válida:

 

public bool NuevoIngreso(double num) //Error. No se puede sobrecargar así

{...}

 

A pesar de que el argumento tiene un nombre distinto (num en lugar de cantidad), es del mismo tipo que el del método del ejemplo, por lo que el compilador tampoco sabría cuál de las dos sobrecargas ejecutar.

 

Bueno, supongo que ahora vendrá la pregunta: ¿Cuál de todas las sobrecargas válidas ejecutará si efectúo la siguiente llamada?

 

MisCuentas.NuevoIngreso(200.53);

 

Efectivamente, aquí podría haber dudas, ya que el número 200.53 puede ser tanto double, como single. Para números decimales, el compilador ejecutará la sobrecarga con el argumento de tipo double. En el caso de números enteros, el compilador ejecutará la sobrecarga cuyo argumento mejor se adapte con el menor consumo de recursos (int, uint, long y unlong, por este orden). Y ahora vendrá la otra pregunta: ¿y si yo quiero que, a pesar de todo, se ejecute la sobrecarga con el argumento de tipo single? Bien, en ese caso tendríamos que añadir un sufijo al número para indicarle al compilador cuál es el tipo de dato que debe aplicar para el argumento:

 

MisCuentas.NuevoIngreso(200.53F);

 

Los sufijos para literales de los distintos tipos de datos numéricos son los siguientes:

 

L (mayúscula o minúscula): long ó ulong, por este orden;

U (mayúscula o minúscula): int ó uint, por este orden;

UL ó LU (independientemente de que esté en mayúsuculas o minúsculas): ulong;

F (mayúscula o minúscula): single;

D (mayúscula o minúscula): double;

M (mayúscula o minúscula): decimal;

 

Argumentos pasados por valor y por referencia

 

Puede que necesitemos que los métodos NuevoIngreso y NuevoGasto devuelvan el saldo nuevo, además de true o false. ¿Podemos hacerlo? Veamos: siendo estrictos en la respuesta, no se puede, ya que un método no puede retornar más de un valor. Sin embargo, sí podemos hacer que un método devuelva datos en uno o varios de sus argumentos. ¿Cómo? Pues pasando esos argumentos por referencia. Me explicaré mejor: un método puede aceptar argumentos de dos formas distintas (en C# son tres, aunque dos de ellas tienen mucho que ver): argumentos pasados por valor y argumentos pasados por referencia.

 

Cuando un método recibe un argumento por valor, lo que ocurre es que se crea una copia local de la variable que se ha pasado en una nueva dirección de memoria. Así, si el método modifica ese valor, la modificación se hace en la nueva dirección de memoria, quedando la variable original sin cambio alguno. Por ejemplo, si hubiéramos escrito el método NuevoIngreso de este modo:

 

public bool NuevoIngreso(double cantidad)

{

    if (cantidad <=0)

        return false;

 

    this.saldo += cantidad;

    cantidad=this.saldo;

    return true;

}

 

Si el saldo era 100, y efectuamos la siguiente llamada, ¿cuál sería la salida en la consola?:

 

double dinero=345.67;

MisCuentas.NuevoIngreso(dinero);

Console.Write(dinero);

 

¿Eres programador de Visual Basic? Pues te has equivocado. La salida sería 345.67, es decir, la variable dinero no ha sido modificada, ya que se ha pasado al método por valor (en C#, si no se indica otra cosa, los argumentos de los métodos se pasan por valor). Veamos qué es lo que ha ocurrido:

 

 

La variable dinero apunta a una determinada zona de memoria. Al pasarse esta variable por valor, el compilador hace una copia de este dato en otra zona de memoria a la que apunta la variable cantidad. Así, cuando se modifica el valor de esta, se modifica en esta nueva zona de memoria, quedando intacta la zona de memoria asignada a la variable dinero.

 

Sin embargo, si escribimos el método del siguiente modo para que reciba los valores por referencia:

 

public bool NuevoIngreso(ref double cantidad)

{

    if (cantidad <=0)

        return false;

 

    this.saldo += cantidad;

    cantidad=this.saldo;

    return true;

}

 

Y modificamos también el código que hacía la llamada:

 

double dinero=345.67;

MisCuentas.NuevoIngreso(ref dinero);

Console.Write(dinero);

 

La salida en la consola sería 445.67. Veamos dónde está la diferencia:

 

 

Fíjate bien en que, ahora, la variable cantidad apunta a la misma zona de memoria a la que apunta la variable dinero. Por este motivo, cualquier modificación que se haga sobre la variable cantidad afectará también a la variable dinero, ya que dichas modificaciones se harán en la zona de memoria reservada para ambas.

 

Sin embargo, las variables que se pasen a un método usando ref deben de haber sido inicializadas previamente, es decir, el programa no se habría compilado si no se hubiera inicializado la variable dinero. Si queremos pasar por referencia argumentos cuyo valor inicial no nos interesa deberíamos usar out en lugar de ref. Por ejemplo, imagina que queremos devolver en otro argumento el valor del saldo redondeado. ¿Para qué? No sé, hombre, sólo es un ejemplo... Habría que hacerlo así:

 

public bool NuevoIngreso(ref double cantidad, out int redondeado)

{

       redondeado=(int) Math.Round(this.saldo);

       if (cantidad <=0)

             return false;

 

       this.saldo += cantidad;

       cantidad=this.saldo;

       redondeado=(int) Math.Round(this.saldo);

       return true;

}

 

Y modificamos también el código que hacía la llamada:

 

double dinero=345.67;

int redoneo;

MisCuentas.NuevoIngreso(ref dinero, out redondeo);

Console.Write(redondeo);

 

Ahora la salida en la consola sería 346. Fíjate que la variable redondeo no ha sido inicializada antes de efectuar la llamada al método (no ha recibido ningún valor). Por otro lado, este argumento debe recibir algún valor antes de que el método retorne, por lo que se asigna antes del if y luego se asigna otra vez después de hacer el ingreso. Sin embargo, la variable dinero sí ha sido inicializada antes de invocar el método, puesto que el método necesitaba saber cuánto había que ingresar, pero no necesita saber nada del valor redondeado, ya que este se calculará a partir del saldo.

 

Métodos static

 

En efecto, por fin vas a saber qué significaba eso de que el método Main tenía que ser static. Bien, los métodos static, son aquellos que se pueden ejecutar sin necesidad de instanciar la clase donde está escrito. La definición de Tom Archer en el capítulo 6 de su libro "A fondo C#" me parece excelente; dice así: "Un método estático es un método que existe en una clase como un todo más que en una instancia específica de la clase". Mucho mejor, ¿verdad? Por lo tanto, el hecho de que el método Main tenga que ser static no es un capricho, ya que, de lo contrario, el CLR no sería capaz de encontrarlo pues antes de que se ejecute la aplicación, lógicamente, no puede haber instancias de ninguna de las clases que la componen.

 

Estos métodos suelen usarse para hacer una serie de operaciones globales que tienen mucho más que ver con la clase como tal que con una instancia específica de la misma: por ejemplo, si tenemos una clase Coche y queremos listar todas las marcas de coches de que disponemos, lo más propio es un método static. ¿Qué necesidad tenemos de instanciar un objeto de esa clase, si solamente queremos ver las marcas disponibles? Otro ejemplo podría ser un método static en la clase Bolígrafo que devolviera una cadena con el nombre del color que le corresponde a un determinado número, ya que no necesitaría instanciar un objeto de la clase para saber si al número uno le corresponde el color negro, o al 5 el rojo, por ejemplo. Por lo tanto, los métodos static no aparecen como miembros de las instancias de una clase, sino como parte integrante de la propia clase. Vamos a poner un pequeño programa completo que ilustre el uso de los métodos static:

 

using System;

 

namespace VisaElectron

{

    class VisaElectron

    {

        public static ushort Limite()

        {

            return 300;

        }

 

        // Aquí irían más miembros de la clase

    }

 

    class VisaElectronApp

    {

        static void Main()

        {

            Console.WriteLine("El límite de la Visa electrón es: {0}",

                VisaElectron.Limite());

 

            string a=Console.ReadLine();

        }

    }

}

 

Para hacer que un método sea static hay que poner esta palabra después del modificador de acceso (si lo hay) y antes del tipo de retorno del método. Este método devolvería el límite que tienen todas las tarjetas Visa Electrón para extraer dinero en un sólo día, (que no sé cuál es, pero límite tienen). Ahora presta especial atención a cómo se invoca este método dentro del método Main (está en negrilla). En efecto, no se ha instanciado ningún objeto de la clase VisaElectron, sino que se ha puesto directamente el nombre de la clase.

 

Por otro lado, soy consciente de que este no es el mejor diseño para esta clase en concreto, pero mi interés principal ahora es que veas muy claro cómo se define un método static y cómo se invoca. Más adelante, cuando empecemos con la herencia, trataremos la redefinición de métodos y el polimorfismo. Por hoy, creo que tenemos suficiente.

 

Sigue este vínculo para bajarte los ejemplos de esta entrega.