Por necesidades del servicio voy a empezar a programar en C#, así que me he leído un par de manuales y he practicado un poco con ello. Y, la verdad, no me ha convencido demasiado. Así que, por petición popular (si por una persona se le puede llamar “popular”), estas son algunas de las conclusiones que he sacado, aunque si eres muy fan de C# quizás no deberías seguir leyendo
:
Declaración de tipos
Extrañamemente se usan formas diferentes para declarar lo mismo. Porque declarar una variable o un nuevo tipo de dato debería ser consistente. Por ejemplo, para declarar una variable se usa <tipo de dato> <identificador>;. En cambio, para declarar un alias de un tipo de dato (no hay nuevos tipos) se usa using <identificador> = <tipo de dato>;. Y, ya para rematar, para declarar un delegate se usa delegate <valor devuelto> <identificador> (<parámetros>);.
¿No sería más sencillo y consistente tener una sintaxis común? Por ejemplo: <tipo de dato> <descripción> <identificador>;, con lo que la declaración quedaría:
// Variable:
int i;
// Tipo de dato:
typedef int MyInt;
// Usando 'using' (sin introducir ninguna palabra reservada adicional):
using int as MyInt;
// Delegate:
delegate void(int) MyDelegate;
Con esto la sintaxis siempre es consistente. Tenemos a la izquierda los tipos y a la derecha, siempre, el nombre tanto de la variable como del tipo nuevo como del delegate (que no deja de ser un tipo nuevo).
Nuevos tipos de datos
En C#, al igual que en C++, no se pueden declarar nuevos tipos de datos. Sí, en serio. Lo que estáis pensando ahora mismo es que con typedef sí se puede… pues no. En realidad es un alias del tipo básico. En C++, este código:
typedef int myint_t;
void overloaded_function(int value);
void overloaded_function(myint_t value); // Error de compilación.
Daría error de compilación porque las dos funciones son iguales. myint_t no es un tipo de datos nuevo, sino un alias de int. En C# ocurre lo mismo, sólo que en lugar de declarar los nuevos tipos con typedef (que no tiene esa palabra reservada) se usa using:
using myint_t = int;
class Main {
void overloaded_funcion(int value) {...}
void overloaded_funcion(myint_t value) {...} // Error de compilación.
}
También se puede hacer alguna triquiñuela, como hacer una clase MyInt que herede de System.Int32. Pero ya habría que implementar ciertas cosas para que funcionase.
Los métodos y su tipo de acceso
Es bastante enrevesado el uso de virtual, override y new para controlar el polimorfismo de las clases. Por ejemplo, en el código:
class A {
public virtual void Who() { Console.WriteLine("A"); }
}
class B : A {
public override void Who() { Console.WriteLine("B"); }
}
class C : B {
public new virtual void Who() { Console.WriteLine("C"); }
}
class D : C {
public override void Who() { Console.WriteLine("D"); }
}
C c = new D();
c.Who(); // Escribe "D"; lógico ¿no?
A a = new D();
a.Who(); // ¡Escribe "B"! ¿Dónde está la lógica de funciones virtuales aquí?
Es obligatorio que en todos los métodos que se podrán sobreescribir en clases derivadas se ponga virtual; que en todos los métodos que sobreescriben se ponga override; y que si un método oculta a otro se ponga new.
¿No sería más sencillo que todos los métodos fuesen virtuales por defecto (porque cuando haces clases, lo más probable es que las vayas a heredar)? ¿No sería más lógico que cuando declaras un método con el mismo nombre en la clase base, automáticamente se sobreescriba el de la base y, en caso de querer llamar al método de la base, uses super, base o similar?
Por cierto, decidme un caso real donde se use esta funcionalidad.
El problema de la clase base frágil
En este manual se menciona el problema de la clase base frágil, que habla de que en Java podría suceder esto, teniendo en principio estas clases:
class BaseClass {
public void CleanUp() {
System.out.println("BaseClass.CleanUp()");
}
}
class DerivedClass extends BaseClass {
public void Delete() {
System.out.println("DerivedClass.Delete()");
}
public void CleanUp() {
Delete();
}
}
public class Test {
public static void main(String[] args) {
BaseClass k = new DerivedClass();
k.CleanUp();
}
}
La salida de este programa sería:
$ javac Test.java && java Test
DerivedClass.Delete();
$
Si luego implementamos el método Delete() en la clase base de esta forma:
class BaseClass {
public void CleanUp() {
System.out.println("BaseClass.CleanUp()");
}
public void Delete() {
System.out.println("Delete all the world!");
}
}
Según el manual, ¡podríamos borrar el mundo!
Esto sería un verdadero problema… si existiese. Pero es muy fácil comprobar que no se produce. La salida con la clase modificada es la misma que sin ella. Sólo usando super.Delete() se podría borrar el mundo.
Ámbito global o local
En C#, si una clase oculta un espacio de nombres, para acceder a dicho espacio introduce una nueva palabra que no está reservada, pero que hay que recordar: global. Por ejemplo:
class Main {
public class System {}
static void Main(string[] args) {
System.Console.WriteLine("¡Hola mundo!"); // Error: la clase 'System' oculta
// el espacio de nombres 'System'.
global::System.Console.WriteLine("Esto sí funciona.");
}
}
¿No sería más sencillo, en lugar de introducir otro identificador nuevo, usar sólo el operador ::, como hace C++ o, incluso, usar el operador .?
Comportamientos extraños de las variables
Resulta que si declaras una variable de tipo delegate, esta se comporta como una lista de delegates que se llaman todos seguidos:
class Test {
void Notify1(string text) {
System.Console.WriteLine("Hello " + text);
}
void Notfiy2(string text) {
System.Console.WriteLine("Goodbye " + text);
}
}
Test test = new Test();
delegate void Notifier(string text);
Notifier notifier;
notifier = new Notifier(test.Notify1);
notifier += new Notifier(test.Notify2);
notifier("Manolito");
// La salida será:
// Hello Manolito
// Goodbye Manolito
¿Nos aclaramos? ¿Notifier es una variable o es una lista de variables? Y si todos los delegates asignados devuelven algún valor, sólo se devuelve el último ejecutado. ¿Y el resto?
Recorriendo listas
C# añade el foreach, que no es más que lo que se conoce como syntactic sugar para el bucle for de toda la vida evitando que el programador use variables índice:
string[] stringArray = new string[] { /* Inicialización */ };
foreach(var s in stringArray) {
System.Console.WriteLn(s);
}
Pero no estoy criticando ese syntactic sugar, todo lo contrario, eso me parece muy bien. Cuanto menos código se escriba y haga más cosas, mejor.
Lo que ya no me parece tan bien es que en ese bucle foreach sólo se tenga acceso a los valores de la lista a recorrer y no se tenga acceso al índice de dicha lista. Sí, simplemente a la famosa i que se omite para que el programador escriba menos código.
Y por culpa de esto, en caso de necesitarla, pues tienes que declararla fuera e incrementarla manualmente. Vamos, con en un bucle for.
Anidamiento
No se permiten métodos anidados, es decir, declaración e implementación de métodos dentro de otros métodos. Sí, venga, vale, se puede “solucionar” mediante el uso de delegates, pero eso no es permitir métodos anidados, eso es una chapuza.
Y además, ya para rematar, tampoco se permite declarar estructuras ni clases dentro de métodos. Como está orientado a objetos, pues toda declaración adicional tiene que ir dentro de la clase.
Actualización 2011-12-05: C y C++ tampoco permiten métodos anidados como me indican en un comentario. Esto lo puse porque yo los he usado para resolver algún problema, sólo que no recuerdo ni el lenguaje ni el proyecto en el que lo hice. ¿Pascal quizás?
Atributos
Los atributos del lenguaje también son otra cosa que está muy bien dentro de un lenguaje. Permiten extender la funcionalidad del compilador, por ejemplo, para hacer compilación condicional, controlar entre diferentes versiones o arquitecturas, marcar elementos como obsoletos… pero, me pregunto, ¿por qué la sintaxis no es coherente con el resto del lenguaje y hay que añadir triquiñuelas del estilo [Conditional("Debug")] al código?
Conclusión
Después de despotricar un poco toca una pequeña conclusión:
Si Microsoft tuvo la oportunidad de crear un nuevo lenguaje de programación desde cero, para ellos solos, sin que nadie se pueda meter, sin que nadie le diera lecciones, con la posibilidad de innovar, de mejorar lo que había hasta el momento ¿por qué lo ha hecho tan retorcido e incoherente? Porque sí, en serio, es retorcido. Hasta C++ —que mira que a Bjarne Stroustrup se le fue la olla cuando lo diseñó— es más consistente y coherente que C#. C# parece un batiburrilo de Java, C++ y alguna funcionalidad adicional como los delegates que más que ser un lenguaje nuevo es lo mismo de siempre con otro nombre. Y propietario, claro.
Y, bueno, de la compilación a código intermedio en lugar de a código máquina directamente ya no hablamos. Eso es una decisión que han tomado pero que tampoco es que importe demasiado. Recordemos que esta generación de código es una de las fases de compilación que se puede cambiar sin afectar a la sintaxis.
Concluyendo, a mi, personalmente, no me convence en absoluto. Pero claro, si quieres hacer un proyecto decente para sistemas Windows, no queda otra que hacerlo en C# o pelearte con los punteros en C++.
O usar Pascal
.
P.D.: Yo, en realidad, soy más del lenguaje de programación D.