Fecha y hora actual: Domingo 25 Ago 2019 05:04
Índice del Foro

Foros de programación informática, diseño gráfico y Web

En esta comunidad intentaremos dar soporte de programación a todos los niveles, desde principiantes a profesionales de la informática, desarrollo de programas, programación web y mucho más.

Programando desde 0: 34- Recursión correcta y Stack

Responder al Tema

Índice del Foro > Programación en general > Programando desde 0: 34- Recursión correcta y Stack

Autor Mensaje
Kyshuo Ayame
Moderador Global


Registrado: 07 Ene 2011
Mensajes: 1043

Mensaje Publicado: Lunes 24 Oct 2011 21:15

Título del mensaje: Programando desde 0: 34- Recursión correcta y Stack

Responder citando

Continuamos entonces con el tema de recursión. Aquí comenzaremos a ver qué hay de fondo en la recursividad, comenzando a aplicarla de forma correcta para, poco a poco, ir comprendiéndola.

Recursión correcta:

¿Cómo lo van llevando? Por el momento tal vez sea difícil porque además de su complejidad, este tema inicialmente no presenta un sentido, o sea, es normal que ustedes se pregunten “¿Y esto para que cornos sirve?”.
La respuesta llegará en su momento cuando lo empecemos a aplicar. Es importante que sepan, que generalmente cada algoritmo iterativo tiene su versión recursiva y viceversa, es decir, cada algoritmo recursivo tiene su versión iterativa. De este modo podríamos afirmar que siempre es posible usar iteración ó recursión, según la preferencia del programador. Esto escapa a algunos casos particulares, pero en lo general se cumple. Ahora bien, si esa afirmación es verdadera ¿por qué molestarse en aprender recursividad si siempre es posible utilizar iteración? Recordar que iteración es utilizar estructuras repetitivas como FOR, WHILE y REPEAT; y que recursión es crear subprogramas que se invocan a sí mismos dentro de su código.

Esta pregunta recoge un poco de la anterior: ¿para qué sirve?. Hay estructuras muy útiles para ciertas cosas que son extremadamente complejas de implementar de forma iterativa. Esto significa que el código sería muy difícil de diseñar y sobretodo de mantener en un futuro. Es decir, si creamos un código iterativo para recorrer un árbol binario (veremos esta estructura más adelante), dicho código sería tan extenso y complicado de depurar para corregir errores que sería un trabajo sumamente tedioso y agotador, y una vez logrado esto, si hubiera que modificarlo en un futuro llevaría un trabajo casi igual al de reprogramarlo. Ni hablar si luego alguien que no fue el propio programador tiene que leer ese código.

De este modo, la recursividad surge como una necesidad.
Obviamente que estos ejemplos que he dado no sirven para nada, o sea, verdaderamente no tiene sentido crear un programa recursivo como Proc, pero es para que vallan masticando de a poco. Realmente la recursividad responde a ese viejo dicho “Divide y vencerás” que mencioné en la introducción a esta parte del curso. Su función real es, dado un problema general de cierta índole, dividirlo en subproblemas que pueden ser resueltos más fácilmente pero qué, inicialmente no tienen solución inmediata, hasta llegar a un caso (caso base) que tiene solución inmediata y puede ser resuelto. A partir de esa resolución se obtienen soluciones a los subproblemas que anteriormente no habían podido solucionarse. Juntando todo esto, se soluciona el problema original.

Igualmente, iré muy lentamente con todo esto. Incluso, habiendo terminado el capítulo de Recursión, ustedes no tendrán aún todo el material que responde a este tema, sino que a lo largo de todo el curso seguirán asimilando más cosas. Sin embargo es sumamente necesario que tengan esta introducción para luego poder trabajar tranquilos.

Veamos entonces un procedimiento recursivo que llamaremos Proc2 que reciba un natural como parámetro X e imprima en pantalla todos los números comprendidos entre X y 0 en forma descendente. Es decir, si recibe el 10 como parámetro, imprimirá: 10 9 8 7 6 5 4 3 2 1 0.

¿Por qué en forma descendente? Porque hacerlo desde 0 a X de forma recursiva complica un poco las cosas, al menos ahora, luego será muy sencillo. Si fuera iterativamente sería fácil ¿no es así? Bien, veremos las dos formas, iterativa y recursiva respectivamente:

Versión Iterativa de Proc2:

Código:
PROCEDURE Proc2(X: CARDINAL);
BEGIN
   WHILE X>=0 DO
      WriteCard(X,1);
      WriteString(“ “);
      X:= X-1;
   END;
END Proc2;


Versión Recursiva de Proc2:

Código:
PROCEDURE Proc2(X: CARDINAL);
BEGIN
   IF X=0 THEN
      WriteCard(X,1);
   ELSE
      WriteCard(X,1);
      WriteString(“ “);
      Proc2(X-1);
   END;
END Proc2;


Bien, como primera cosa, es importante asumir que el procedimiento Proc2 recibe siempre un valor válido. Esto en general se verifica antes de hacer el llamado. Es lo mismo que ustedes deben hacer al llamar, por ejemplo, a WriteCard; obviamente nunca lo llamarán pasando un parámetro que no sea CARDINAL, de lo contrario no funcionará.

Bien, la versión iterativa no implica ningún problema porque justamente esa es la idea, no hacerse problema en entender lo que ya conocen y enfocarse a lo nuevo.

Es importante, antes de comenzar a analizar paso a paso este caso, notar que este procedimiento tiene un IF...ELSE principal. Siempre todo subprograma recursivo tendrá un IF...ELSE principal que incluso puede tener más anidaciones ELSE. La forma más básica es un IF y un ELSE. ¿Por qué? Bien, porque para la recursión nosotros tenemos un problema general que puede ser dividido en subproblemas, y siempre ha de haber un caso base, el cual se resuelve inmediatamente. De este modo, en el IF diferenciamos entre el caso base y el resto. Así, podríamos decir que una forma general de la recursión es:

Código:
IF caso base THEN
   tareas a realizar con el caso base;
ELSE
   tareas a realizar con un caso común;
   llamado recursivo;
END;


En este caso, nuestro problema es imprimir todos los números desde un X mayor que 0 hasta 0. Los subproblemas son imprimir un número X dado, y el caso base es pues, imprimir el 0. ¿Cómo llego a esto? Bien, si el número es mayor que 0, necesitamos imprimirlo y luego imprimir el anterior a él. Por ejemplo, si tenemos el 5 necesitamos imprimirlo en pantalla y luego imprimir el 4, para luego imprimir el 3, y así. Sin embargo, si tenemos el 0 solo necesitamos imprimirlo y ya, no hay más nada por hacer. Por eso es el caso base, porque se resuelve inmediátamente; cualquier otro caso no resuelve el problema total.

Identificar esto es crucial y es una de las principales dificultades de la recursividad. El caso base será siempre aquel que luego de ser resuelto no implica más tarea a resolver. En muchos casos puede haber más de un caso base, por eso el IF...ELSE principal puede ser más extenso que el de este ejemplo.

No está de más decir que en los casos base no hay llamado recursivo, por eso son casos base, porque se resuelven de inmediato y no dejan nada por hacer.

Asumamos entonces que Proc2 es llamado inicialmente así: Proc2(10).

------------1. Entramos en Proc2 con X=10. Como X no es igual que 0, o sea, no es el caso base, entonces pasamos al ELSE de nuestro IF...ELSE.

Imprimimos el valor de X, luego imprimimos un espacio en blanco y llegamos al llamado recursivo: Proc2(X-1). Esto equivale a Proc2(10-1) lo cual es Proc2(9).

Observen entonces que hemos resuelto una parte de nuestro problema, y pasamos el siguiente problema a un nuevo llamado recursivo. O sea, inicialmente el problema era imprimir desde 10 hasta 0, pero en el llamado recursivo el problema será desde 9 hasta 0.

------------2. Entramos en Proc2 ahora con X=9. Como no es el caso base, pasamos al ELSE. Imprimimos el 9, un espacio en blanco y tenemos un nuevo llamado recursivo Proc(X-1), lo cual en este caso es Proc2(9-1) que equivale a Proc2(Sol.


------------3. Siguiendo esto, irá apareciendo en pantalla primero el 10, luego el 9 y en este caso el 8. Si entienden como va funcionando, avancemos entonces hasta que entramos con Proc(1).

En ese caso entonces X vale 1, no es el caso base por lo cual vamos al ELSE. Imprimimos el 1, luego el espacio en blanco y llamamos a Proc(X-1) lo cual es Proc(1-1) que equivale a Proc(0).

------------4. Llegamos entonces al caso base. X vale 0, entonces entramos en el IF y no en el ELSE. Ahí imprimimos el valor de X y listo, no hay nada por hacer ni llamado recursivo. Con lo cual Proc2 ha terminado sus tareas.


Entonces, habiendo identificado el caso base debemos garantizar que, dado cualquier otro caso, nuestro algoritmo recursivo llegará al caso base. Si esto no sucediera, entonces la recursividad no terminará y nuestro programa se cerrá con el error Stack Overflow.

Como ejercicio ejecuten este código con el depurador, llamando a Proc2 con un valor pequeño, por ejemplo 5, y vean lo que pasa con el Stack de memoria cuando la recursión termina. Sigan paso a paso con F7 hasta que la ejecución del programa finalice. Realmente el depurador ayuda mucho a entender cómo funcionan estas cosas.

-------------------------------------------------------------------------------------

Concepto de Pila (Stack):

He nombrado hasta ahora muchas veces esto del Stack de memoria y el error Stack Overflow (Desbordamiento de pila). Stack en inglés significa Pila. Al igual que las Listas Encadenadas, una Pila es una estructura dinámica.

En este momento solo les dejaré un concepto básico de lo que es una Pila, pero más adelante les enseñaré a implementarlo ya que trabajaremos con esto. Para explicar bien lo que es esta estructura, asumo que saben lo que son listas encadenadas simples. En esa estructura ustedes podían insertar un objeto al inicio, al final, obtener un objeto del final y/u obtener el objeto del inicio (esto lo veremos más detalladamente más adelante). En una pila los objetos se insertan siempre en el inicio y se sacan siempre desde el inicio.

Imaginen una pila de platos. Si van a poner un plato más en la pila ¿donde lo ponen? Podrían levantar todos los platos y poner el nuevo plato abajo del todo, podrían poner el nuevo plato por algún lugar intermedio de la pila, o bien podrían poner el nuevo plato en el extremo superior.

Claramente esto es lo más cómodo, tanto para poner un nuevo plato como para sacar uno. Siempre ponemos en el extremo superior y siempre quitamos desde el extremo superior. De esta manera, siempre el último en entrar es el primero en salir. Esto se conoce en programación como LIFO (Last In First Out – Último en entrar primero en salir), es decir, el último plato colocado en la pila será luego el primero en ser quitado.



Siendo así, una pila puede, o bien estar vacía o bien tener al menos un elemento. En programación esta estructura es muy utilizada. Piensen por ejemplo en la función Deshacer de cualquier programa, ¿qué hace? Pues deshace la última acción realizada. Esa función tiene detrás una pila, o sea, el programa guarda en una pila las acciones que vamos realizando, de este modo, siempre en el tope de la pila de acciones está lo último que hemos realizado. Por ejemplo, imaginen que en un programa de dibujo realizamos las siguientes acciones en este orden: dibujamos un círculo, dibujamos un cuadrado, dibujamos un óvalo, pintamos el circulo, borramos el cuadrado.

Entonces, cuando dibujamos el círculo el programa toma esa acción y la guarda en la pila de acciones:



Cuando dibujamos el cuadrado el programa toma esa acción y la añade a la pila:



De este modo, al realizar todas esas acciones, la pila queda así:



Como podrán observar, tenemos todas las acciones realizadas en orden inverso. Cuando presionamos en Deshacer, el programa va al tope de la pila de acciones y elimina lo que haya allí, ya que de este modo estará eliminando la última acción realizada.

Esta pila puede tener un tope, o sea, una cantidad máxima de elementos. ¿Qué pasa cuando se llena? Pues las opciones serían varias, por ejemplo, no agregar más nada. Para este ejemplo eso no sería muy útil. Lo que hacen los programas en estos casos es quitar lo que está al final de la pila (abajo del todo) y agregar un nuevo elemento al principio. De este modo vamos perdiendo acciones realizadas, y en efecto, eso es lo que pasa. Ustedes podrán deshacer hasta cierta cantidad de acciones, luego no podrán hacerlo más, por ejemplo, el programa Paint nos permite solo deshacer hasta tres acciones. De este modo, en el ejemplo, la pila de acciones de Paint iría hasta DIBUJAR ÓVLAO inclusive. Si quisiéramos deshacer el dibujo del cuadrado o del círculo no podríamos.

-----------------------------------------------------------------------------------

El Stack de Memoria:

Teniendo un concepto básico acerca de lo que es una pila y de como funciona, veremos entonces qué es el Stack de memoria, es decir, qué es la pila de memoria. Esto resulta crucial para comprender a fondo cómo funciona la recursividad, ya que a pesar de que los ejemplos que hemos visto hasta ahora son sencillos, realmente esto se complicará bastante.

Veamos un ejemplo bien sencillo, olvidando momentáneamente de la recurrencia, en el cual haremos dos llamados a procedimientos declarados en el programa. Dichos procedimientos serán bien tontos, o sea, no harán nada interesante: uno, llamado Proc1 recibirá un CARDINAL X menor que 60 e imprimirá una línea de X asteriscos en pantalla, y el otro, llamado Proc2 recibirá un CARDINAL como parámetro y mostrará en pantalla un mensaje diciendo si dicho número es par o no.

Código:
MODULE EjemploSencillo;

FROM STexIO IMPORT WriteString, WriteLn;

PROCEDURE Proc1(X: CARDINAL);
VAR i: CARDINAL;
BEGIN
   FOR i:= 0 TO X DO
      WriteString(“*”);
   END;
END Proc1;

PROCEDURE Proc2(X: CARDINAL);
BEGIN
   IF X MOD 2= 0 THEN
      WriteString(“Es un número par.”);
   ELSE
      WriteString(“Es un número impar.”);
   END;
END Proc2;
(************Programa principal**************)
BEGIN
   WriteString(“Bienvenid@ a EjemploSencillo”); WriteLn; WriteLn;
   Proc1(10);
   Proc1(50);
   Proc2(10);
END EjemploSencillo;


Bien, a pesar de que yo explicaré el por qué de este código lo que sucede por detrás de Modula en su ejecución, quiero que primero corran este código con el Debugger mirando lo que sucede con el Stack. No importa que no sepan qué es lo que hay allí, quiero que miren cuantos elementos hay en esa pila.

Si lo hacen, podrán observar que al iniciar el programa ya hay “cosas” en esa pila. ¿Qué son esas cosas? Pues son las variables y estructuras que requiere nuestro programa para funcionar, o sea, inicialmente el programa tiene lo que se conoce como estado. El estado de un programa implica la cantidad de variables, tipos, constantes y sus valores. Modula crea un registro de ese estado y guarda esa información en el Stack de memoria para luego acceder a ella si hace falta, modificarla, etc. Este Stack, a pesar de estar en memoria, no es la misma memoria que almacena los objetos que creamos, sino que en realidad guarda las direcciones de memoria donde están los datos para que el programa pueda acceder a ellos. Como quien dice, esta pila guarda punteros hacia la información del programa.

Bien. Cuando nosotros hacemos un llamado a un procedimiento o una función, el control del programa es cedido al procedimiento o función. O sea, el programa principal cede el control al subprograma. En este ejemplo, el programa principal inicia en WriteString. Por tanto, momentáneamente el control es cedido a WriteString y cuando este termina el programa avanza hacia la siguiente instrucción. Luego tenemos dos WriteLn, pero no me interesan. Lo importante es cuando llegamos a Proc1. Allí el programa principal cede su control dándoselo a  Proc1, por eso si ustedes van con el depurador pueden ver que la ejecución pasa a estar dentro de Proc1. Si han hecho lo que les pedí, podrán ver que cuando entramos a Proc1 la cantidad de “cosas” en el Stack aumenta, Porc1 hace lo que debe hacer y cuando el control es devuelto al programa principal la cantidad de “cosas” en el Stack disminuye nuevamente.

Esto es porque lo que hace el programa es crear un registro de su estado antes de entrar a Proc1 para “acordarse” de cómo estaban los datos en ese momento, de modo que al salir de Proc1 todo pueda continuar tranquilamente. Además se “acuerda” de en qué parte fue invocado Proc1 para luego seguir desde allí su ejecución.

Entonces, llegamos a la segunda línea de nuestro bloque principal donde está el llamado a Proc1(10). Ahí, nuestro programa crea un registro de su estado y lo guarda en el Stack. De este modo sabe como estaban las cosas en ese momento y dónde fue llamado Proc1 para luego continuar justamente desde allí.

Entramos en Proc1 y hacemos lo que hay que hacer, luego salimos. Entonces nuestro programa va al Stack, se fija en los últimos datos que hay guardados allí y los usa para actualizar su estado y para acordarse que tiene que continuar desde la segunda línea del bloque principal. Como ha usado esos datos, borra el registro del Stack porque ya no hace falta y continúa su curso. Por eso, al salir de un procedimiento el Stack pierde contenido. Como pueden ver, los programadores de Modula 2 la han tenido difícil. Igual así los de Pascal y los de cualquier lenguaje que implemente esta forma de “recordar” sus estados.

Llegamos a la línea 3 del bloque principal, como es un nuevo llamado a un procedimiento, en este caso nuevamente a Proc1 pero con otro parámetro, el programa vuelve a generar un registro de su estado, desde donde fue llamado Proc1 (en la línea 3), y lo guarda la pila de memoria. Al salir de Proc1 nuestro programa se fija lo que hay al final del Stack para saber cómo estaban las cosas en ese momento y desde donde debe continuar su ejecución. Como ya usó esos datos de la pila, los borra.

Así funciona esto. Sin embargo ¿qué pasa si desde un subprograma llamamos a otro? Pues bien, veamos otro ejemplo sencillo donde tendremos una función llamada CalcularAreaTriangulo que recibirá como parámetro dos valores de tipo REAL que serán la base y la altura del triángulo. Tendremos también una función booleana llamada SePuedeCalcularArea que recibirá dos REALES y devolverá TRUE si ningún valor es igual que 0 y FALSE en caso contrario.
La función CalcularAreaTriangulo hará un llamado a SePuedeCalcularArea para asegurarse de que calculará un área válida. Si SePuedeCalcularArea devuelve el valor FALSE, CalcularAreaTriangulo retornará un -1.

Código:
MODULE EjemploSencillo2;

FROM SRealIO IMPORT WriteReal, ReadReal;
FROM STextIO IMPORT WriteString, SkipLine;

VAR base, altura: REAL;

(*-----------------Función SePuedeCalcularArea--------------------*)
PROCEDURE SePuedeCalcularArea(b, h: REAL): BOOLEAN;
BEGIN
   RETURN (b<>0.0) AND (h<>0.0);
END SePuedeCalcularArea;
(*------------------------------------------------------------------*)

(*-----------------Función CalcularAreaTriangulo--------------------*)
PROCEDURE CalcularAreaTriangulo(b, h: REAL): REAL;
BEGIN
   IF SePuedeCalcularArea(b,h) THEN
      RETURN b*h/2.0;
   ELSE
      RETURN -1.0;
   END;
END CalcularAreaTriangulo;
(*------------------------------------------------------------------*)
(*PROGRAMA PRINCIPAL*)
BEGIN
   WriteString(“Ingrese la base de un triángulo: “);
   ReadReal(base); SkipLine;
   WriteString(“Ingrese la altura del triángulo: “);
   ReadReal(altura); SkipLine;
   WriteString(“El área del triángulo es: “);
   WriteReal(CalcularAreaTriangulo(base,altura),1);
END EjemploSencillo2;


Este programa no es para nada complejo, seguramente si ustedes lo leen lo comprenderán enseguida. Lo que yo quiero que vean aquí es lo que pasa con el Stack de memoria.

Entonces, vallamos a eso: Nuestro programa inicia en el BEGIN del bloque principal. Ahí se crea un registro con su estado. Tenemos un procedimiento WriteString, y aunque esto es un subprograma no crea registro de memoria porque es nativo del lenguaje y Modula lo maneja a su manera, por eso yo enfoco la atención en los subprogramas que nosotros creamos. Como segunda instrucción tenemos un ReadReal, como tercera instrucción un SkipLine. Luego tenemos como cuarta instrucción un WriteString, seguido de un ReadReal y luego un SkipLine. Como séptima instrucción tenemos otro WriteString hasta que finalmente llegamos al WriteReal final. Este me interesa porque justamente es el procedimiento que tiene el llamado a nuestra función CalcularAreaTriangulo. No está de más notar que puedo incluir el llamado a esta función dentro del WriteReal porque CalcularAreaTriangulo retorna un real y es justamente lo que WriteReal espera allí.

Bien, cuando llegamos al último WriteReal nuestro programa ve que hay una invocación a CalcularAreaTriangulo y por tanto esta debe ser resuelta. De este modo genera un registro de memoria para recordar el valor de las variables en ese momento, en qué lugar fue llamada CalcularAreaTriangulo, etc, y guarda eso en la pila de memoria. Entramos en CalcularAreaTriangulo y tenemos un llamado a SePuedeCalcularArea. Entonces ahí nuestro programa genera otro registro de memoria para recordar como estaban las cosas hasta el momento y dónde fue llamada SePuedeCalcularArea para luego seguir desde allí. Este nuevo registro se guarda luego del anterior ya que es parte de una pila.

Entramos en SePuedeCalcularArea y hacemos lo que hay que hacer. Al terminar, nuestro programa va a la pila de memoria y dice “A ver qué es lo que tengo que hacer ahora... ¡¡¡Ah sí!!! Tengo que continuar desde el IF que está dentro de CalcularAreaTriangulo con los valores que tienen b y h ahí dentro”. Entonces borra el último registro de la pila y continúa su curso. Al terminar de ejecutar CalcularAreaTriangulo vuelve a la pila y dice “¿Y ahora qué me falta? Ah sí, como CalcularAreaTriangulo fue llamada desde dentro de WriteReal debo imprimir su valor de retorno”. Entonces borra el último registro del Stack y continúa su curso hasta finalizar.

Entonces, lo que yo quiero que les quede como idea es que: Cada vez que llamamos a un subprograma nuestro programa guarda en el Stack un registro para acordarse desde donde fue llamado dicho subrpograma y qué valores tenían las variables en ese momento. De ese modo sabe desde dónde debe continuar y con qué valores contaba.
-------------------------------------------------------------------------------------

Hemos terminado por hoy. Espero que vayan comprendiendo poco a poco todo esto.

Saludos.



Ultima edición por Kyshuo Ayame el Lunes 08 Abr 2013 21:38; editado 2 veces
Volver arriba
Ver perfil del usuario Enviar mensaje privado
LongShot



Registrado: 05 Abr 2013
Mensajes: 4

Mensaje Publicado: Lunes 08 Abr 2013 21:34

Título del mensaje: Re: Programando desde 0: 34- Recursión correcta y Stack

Responder citando

Hola! Me registro para agradecer el gran aporte. Los temas están super claros! MUCHAS GRACIAS!!!!!!!!!!! Risa tonta
Paso a comentar. Creo que en el siguiente codigo falta un x:=x-1 para que termine.

Versión Iterativa de Proc2:

Código:
PROCEDURE Proc2(X: CARDINAL);
BEGIN
WHILE X>=0 DO
WriteCard(X,1);
WriteString(“ “);
END;
END Proc2;

Saludos

Volver arriba
Ver perfil del usuario Enviar mensaje privado
Kyshuo Ayame
Moderador Global


Registrado: 07 Ene 2011
Mensajes: 1043

Mensaje Publicado: Lunes 08 Abr 2013 21:39

Título del mensaje: Re: Programando desde 0: 34- Recursión correcta y Stack

Responder citando

Toda la razón... ya lo corregí. Muchísimas gracias.

Volver arriba
Ver perfil del usuario Enviar mensaje privado
mrasd



Registrado: 26 Abr 2013
Mensajes: 8

Mensaje Publicado: Jueves 09 May 2013 20:51

Título del mensaje: Re: Programando desde 0: 34- Recursión correcta y Stack

Responder citando

Código:
MODULE EjemploSencillo;
.
.
.


A StexIO le falta la "t", para quedar STextIO

Volver arriba
Ver perfil del usuario Enviar mensaje privado
Responder al Tema
Mostrar mensajes anteriores:   
Ir a:  
Todas las horas están en GMT + 2 Horas

Temas relacionados

Tema Autor Foros Respuestas Publicado
El foro no contiene ningún mensaje nuevo

Buenas desde el sur del sur =)

Maugarni Preséntate a la comunidad 1 Jueves 22 Ago 2019 14:09 Ver último mensaje
El foro no contiene ningún mensaje nuevo

Hola desde bcn

Dav2k6 Preséntate a la comunidad 2 Miércoles 26 Jun 2019 19:22 Ver último mensaje
El foro no contiene ningún mensaje nuevo

Existen problemas al descargar musica desde you...

SusanaP Tu PC 2 Martes 26 Mar 2019 19:22 Ver último mensaje
El foro no contiene ningún mensaje nuevo

hola!! los saludo desde argentina

mery Preséntate a la comunidad 2 Jueves 13 Dic 2018 17:28 Ver último mensaje
El foro no contiene ningún mensaje nuevo

Llamada a web service desde form

mrrobot2 Programación Web en general 1 Martes 14 Nov 2017 00:50 Ver último mensaje
Panel de Control
No puede crear mensajes, No puede responder temas, No puede editar sus mensajes, No puede borrar sus mensajes, No puede votar en encuestas,