Si anteriormente os hablamos del azúcar sintáctico y de cómo facilita la escritura de código, haciendo más fácil su comprensión, hoy os hablaremos de la sal sintáctica, cuya misión es justo la contraria: hacer más tediosa la labor del programador.
¿Y qué finalidad podría tener el hacer que programar sea una tarea más engorrosa? Bien usada, y especialmente en estructuras complejas, sirve para obligar al programador a escribir todo el código necesario y no sólo el mínimo, demostrando así que verdaderamente conoce la estructura en cuestión y sabe cómo debe usarla. Así pues, la sal sintáctica son esos granos de sal gorda que nos pueden resultar desagradables cuando nos los encontramos de repente en la comida, pero a los que tenemos que agradecerle que la misma haya llegado hasta nosotros sin pudrirse.
Dicho de otra manera, se trata de complicar la sintaxis de alguna estructura para evitar que puedas hacer barrabasadas con ella. La letra con sangre entra, que dirían algunos.
Ejemplos de sal sintáctica
Seguramente a todos se os haya venido a la cabeza el ejemplo más genérico y extendido de sal sintáctica: el tipado fuerte. Si PHP o JavaScript son capaces de inferir el tipo de una variable, ¿cómo es posible que C o Java no sepan?
Ése es el quid de la cuestión: no es que no sepan, es que no quieren. Y no por perezosos, sino porque esperan que tú, como programador, sepas qué tipos de datos estás usando y qué esperas de las operaciones que realices con ellos. De esta manera, puedes lograr hacer programas más eficientes, ya que al escribirlo definirás los tipos exactos que necesitas usar y no se malgastará memoria porque en tiempo de ejecución se decida usar un long int para almacenar números pequeños, por ejemplo.
Casting explícito en Java vs implícito en C/C++
Al hilo del tipado fuerte, existen restricciones que van más allá de la declaración de la variable. Si en C o C++ se asigna a un entero el valor de un real con coma flotante, éste se truncará, despreciándose la parte decimal y almacenándose sólo la parte entera.
Pero en Java eso no está permitido. Si intentas esa asignación, el compilador te advierte que debes realizar tú mismo el casting. No es una labor que lleve demasiado tiempo ni que ensucie demasiado el código. Al contrario, sirve para recalcar a simple vista que el resultado que vas a almacenar en la variable entera no tiene por qué ser exactamente el que tenías en la flotante. Es decir, sirve para que el programador tenga un conocimiento más exhaustivo de lo que su propio programa hace y que de este modo sea más probable detectar errores en tiempo de compilación en lugar de encontrárselos en tiempo de ejecución.
// En C/C++ float pi = 3.14; int ePi = pi; printf("%d", ePi); // Imprimirá 3
// En Java float jPi = 3.14; int jePi = pi; // Error de compilación int jePi2 = (int)pi; System.out.println(jePi2); // Imprimirá 3
El hecho de advertirnos que debemos realizar el casting puede servir además para que recapacitemos acerca de si ésa es la mejor opción, o si por el contrario nos conviene más aplicar Math.round()
para obtener el entero más cercano.
Switch en C#
Un problema de trabajar con varios lenguajes similares es que a veces las estructuras se parecen mucho pero tienen sutiles diferencias. Y el diablo está en los detalles.
La sentencia switch en C tiene una estructura y funcionamiento poco intuitivos: busca la primera etiqueta case
que cumpla la condición y a partir de ahí empieza a aplicar todas las siguientes en cascada, a menos que se encuentre con un break
que la interrumpa.
Dado que en la inmensa mayoría de casos no queremos ejecutar las condiciones en cascada, sino simplemente elegir una, lo habitual es encontrarnos tantos breaks como opciones individuales. Por eso, al desarrollar C#, Microsoft decidió eliminar el procesamiento en cascada que prácticamente nadie usa.
El problema es que la mayoría de programadores venían de programar con lenguajes como C/C++, y para que les quedase claro que no era posible ejecutar todas las opciones del tirón, optaron por imitar la estructura conocida por los desarrolladores: el break
es parte irrenunciable del switch, incluso en el caso por defecto.
switch (valor) { case 1: Console.WriteLine("Caso 1"); break; case 2: Console.WriteLine("Caso 2"); break; default: Console.WriteLine("Caso por defecto"); break; }
Según la referencia de switch en MSDN, C# requiere que el final de las secciones switch, incluida la última, sea inalcanzable. Esto permite que en lugar del break
podamos meter otra sentencia que rompa el flujo normal, como puede ser un return
, throw
o incluso un goto case
, que precisamente es el que nos permitiría imitar el comportamiento del switch sin brakes de C.
Ocultación de miembros heredados en C#
Para exponer el uso, primero planteemos un escenario en el que necesitemos ocultar algunos miembros. Supongamos que tenemos una clase llamada Persona que no tiene ningún propósito particular, por lo que apenas contiene un par de atributos.
public abstract class Persona { public DateTime fechaNacimiento; public int altura; public string nombre; public string sexo; public string imprimeDetalles() { return nombre + " es una persona de sexo " + sexo + " nacida el " + fechaNacimiento; } }
La variable sexo debería ser de un tipo enumerado, pero la dejaremos como cadena para abstraernos del lenguaje concreto que estamos utilizando. Por otra parte, el método imprimeDetalles()
sería similar al toString()
de algunos lenguajes.
Ahora imaginemos que necesitamos una clase llamada Empleado para representar a los trabajadores de una empresa. Podemos hacer que esa clase herede de Persona y añada algunos miembros nuevos, como el salario anual o la fecha de entrada en la compañía. Pero al mismo tiempo, nos interesa ocultar el atributo sexo, ya que no sólo es irrelevante para su puesto de trabajo, sino que además podría ser discriminatorio el hecho de tenerlo en cuenta para cualquier operación, así que optamos por ocultarlo dándole un valor neutro:
public class Empleado:Persona { public DateTime fechaIncorporacion; public int salario; private string sexo = "no definido"; public string imprimeDetalles() { return nombre + " trabaja en la compañía desde " + fechaIncorporacion; } }
Por último, imaginemos que la empresa organiza unos juegos atléticos entre sus trabajadores, para los que utilizaremos una clase derivada de Empleado:
public class Participante:Empleado { public string prueba; public string imprimeDetalles() { return nombre + " participa en la modalidad " + sexo + " de la prueba " + prueba; } }
Este tipo de declaración de miembros que hemos realizado será válida en la mayoría de lenguajes. Sin embargo, C# nos advertirá mediante un warning que estamos haciendo una ocultación implícita de miembros de la clase base. Tengamos en cuenta que un mismo atributo o función pudo haberse incluido antes en una de las clases hijas y posteriormente en la clase madre, por lo que este warning nos pretende avisar de que quizá estemos redefiniendo sin querer esos atributos o miembros.
Para evitar ese warning en un código que compila y se ejecuta correctamente tenemos dos opciones. Por una parte podemos usar la palabra reservada override
para redefinir un miembro. Por ejemplo, sería una buena opción usarla con el método imprimeDetalles()
, ya que su implementación en la clase base no nos vuelve a ser útil en las derivadas.
public class Participante:Empleado { public string prueba; public override string imprimeDetalles() { return nombre + " participa en la modalidad " + sexo + " de la prueba " + prueba; } }
En cuanto al atributo sexo
, ya que no nos interesa en la clase derivada Empleado pero sí en alguna de sus clases hijas, en lugar de redefinirlo, optaremos por ocultarlo. Para ello usaremos la palabra reservada new
, que además aplica sólo al ámbito donde se define, por lo que al acompañar a un atributo privado hará que en las clases derivadas vuelva a ser accesible el valor de la clase base Persona:
public class Empleado:Persona { public DateTime fechaIncorporacion; public int salario; new private string sexo = "no definido"; public override string imprimeDetalles() { return nombre + " trabaja en la compañía desde " + fechaIncorporacion; } }
Entonces, ¿cuál es la diferencia entre utilizar o no la palabra new
? A efectos prácticos: ninguna. La compilación será idéntica y generará el mismo ejecutable. Su uso es un ejemplo más de sal sintáctica, donde una implementación con más texto es preferible para que al programador no le quepa duda de que está ocultando explícitamente un método de la clase base. Como indica la referencia del modificador new, lo único que obtenemos al usarla es que no se nos muestre este warning:
The keyword new is required on 'Empleado.sexo' because it hides inherited member 'Persona.sexo'.
Ejemplos menores
No hace falta buscar escenarios tan enrevesados para entender otras simples obligaciones que los lenguajes nos imponen con la finalidad de que nos sea más difícil cometer errores al programar:
El punto y coma de muchos lenguajes, con el que nos aseguramos de haber terminado la sentencia y que no se nos ha quedado a medias.
Los cierres de bloque que vuelven a incluir la palabra de apertura: End IF, End While, End Do...
El número de línea en BASIC y algunos de sus derivados.
Alternativa a la sal sintáctica
Al igual que como alternativa al azúcar había quien mencionaba el sirope o la sacarina, también existen algunos autores que hablan de matarratas sintáctico para referirse a esa sal que complica tanto la escritura del código que, lejos de ser una ayuda para codificar bien, acaba siendo una trampa por la cual se obtiene un código obtuso y difícil de mantener.
Por último, cabe señalar que las distintas estrategias de los lenguajes hacen que la sal sintáctica se nos pueda presentar en forma de errores de compilación o simplemente como warnings, pero que en cualquier caso suelen ser lo suficientemente explicativos como para no nos resulte difícil corregirlos y de paso aprender a usar la estructura en cuestión.
En Genbeta Dev | Programar sería horrible sin el azúcar sintáctico (I)
Imagen | Wikimedia
Ver todos los comentarios en https://www.genbeta.com
VER 0 Comentario