Bueno, he aquí un dilema que es más o menos como el tipado estático vs el dinámico o el preferir espacios o tabuladores: es decir, preferencia personal. Pero a veces las preferencias personales se ven influenciadas por lo que conocemos (o más precisamente por lo que desconocemos)… Así que dejadme que os cuente cuatro cosas al respecto y ya si eso, luego lo discutimos en los comentarios o en un bar…
Categoría: C#
C#9 – Type classes y extensiones
Estaba yo revisando algunas de las nuevas características que quizá incorpore C# 9 y me he encontrado con la propuesta de type classes (shapes en la teminología de C#), que me parece bastante interesante y sobre la cual me gustaría hacer algunos comentarios 🙂
Comparaciones en C#
¡Buenas!
Este post pertenece al «calendario de adviento de C#«, y me gustaría hablaros de un tema que parece sencillo pero que bueno, esconde sus cosillas. En concreto sobre comparaciones en C#.
C#: Conversiones (explícitas o implícitas) e interfaces
Una de las características más útiles, aunque más potencialmente peligrosas de C# es la posibilidad de sobrecargar los operadores de conversión (casting) y concretamente el de conversión implícita.
Poder sobrecargar el operador de conversión explícita, aunque lo entiendo como una característica que agrega ortogonalidad al lenguaje, no es algo que me guste. Antes de eso prefiero crear un método AsXXX(). De hecho me parece que un cliente de mi clase encontrará más lógico un método AsXXX() que no «un casting a XXX» que debes saber que se puede hacer para hacerlo.
Continúa leyendo C#: Conversiones (explícitas o implícitas) e interfaces
C#: Structs de un solo campo como typedefs
No hace mucho me preguntaba si usar structs de un solo campo tenía alguna penalización respecto a usar, simplemente, una variable del tipo del campo. Es decir, me preguntaba si tener:
struct Sint { public int value; }
Tenía alguna penalización al respecto de usar, simplemente, una variable int.
A nivel de memoria sospechaba que no: una struct ocupa lo mismo que la suma de todos sus campos, más los paddings que se agregan para que los campos estén alineados, más el padding final que se agrega para que, en el caso de un array, los elementos estén alineados. En el caso de una estructura que solo tenga un int esos paddings no existen (*):
namespace PaddingTest { struct NoPadding { public int i1; } struct Padding { public bool b1; } class Program { static void Main(string[] args) { Console.WriteLine($"{sizeof(int)} vs {Marshal.SizeOf<NoPadding>()}"); Console.WriteLine($"{sizeof(bool)} vs {Marshal.SizeOf<Padding>()}"); Console.ReadLine(); } } }
Si ejecutas este código, lo más habitual es que veas la primera línea «4 vs 4» y la segunda línea «1 vs 4». Eso es porque, a pesar de que un bool ocupa un solo byte, una estructura que contenga un bool ocupa 4 bytes, debido al padding para alinear la memoria. Esos alineamientos son importantes, ya que no tener los datos alineados supone una pérdida de rendimiento importante (el procesador debe efectuar operaciones adicionales).
(*) Cuando digo que usando un int esos paddings no existen me estoy refiriendo a mi arquitectura (x64). Eso es importante. Observa que no uso sizeof para medir el tamaño de una struct. Eso es porque no se puede. Las structs no tienen un tamaño predefinido (a diferencia de los tipos propios como Int32 que siempre son 32 bits). Eso es debido, precisamente a ese padding: el CLR puede decidir de empaquetar una estrucutra con distintos paddings en función de la arquitectura sobre la cual se ejecuta el programa y por ello, al no ser el tamaño constante, no podemos usar sizeof.
La otra pregunta era si el rendimiento afectaba. Encontré este post donde parece ser que demuestra que no. De todos modos el test que se hace en este post se limita a comprobar si acceder a un campo de la estructura penaliza. Pero yo quiero más que eso: yo quiero ver si recibir, crear y devolver la estructura tiene impacto en el rendimiento. En definitiva quiero comprobar la diferencia entre esas dos funciones:
static int IntOp(int i) { var j = i + 10; return j; } static Sint SintOp(Sint i) { var j = new Sint(i.Value + 10); return j; }
La primera opera solo con ints y la segunda solo con mi estructura (que solo tiene un int). Así que elaboré un test que ejectuaba esas funciones 100 millones de veces:
struct Sint { public readonly int Value; public Sint(int v) => Value = v; } class Program { static void Main(string[] args) { const int MAX_LOOP= 100000000; const int MAX_REPS = 10; long[] intTimes = new long[MAX_REPS]; long[] sintTimes = new long[MAX_REPS]; for (var rep = 0; rep<MAX_REPS; rep++) { var (tint, tsint) = Test(MAX_LOOP); intTimes[rep] = tint; sintTimes[rep] = tsint; } for (var idx=0; idx < MAX_REPS; idx++) { var diff = sintTimes[idx] - intTimes[idx]; var percent = ((double)diff / (double)intTimes[idx]) * 100; Console.WriteLine($"int took {intTimes[idx]}. Sint took {sintTimes[idx]}. Diff is {diff} ({percent}%)"); } Console.ReadLine(); } private static (long, long) Test(int times) { var s = new Stopwatch(); s.Start(); for (var idx = 0; idx < times; idx++) { var r = IntOp(idx); } s.Stop(); var ms1 = s.ElapsedMilliseconds; s.Reset(); s.Start(); for (var idx = 0; idx < times; idx++) { var r = SintOp(new Sint(idx)); } s.Stop(); var ms2 = s.ElapsedMilliseconds; s.Reset(); return (ms1, ms2); } static int IntOp(int i) { var j = i + 10; return j; } static Sint SintOp(Sint i) { var j = new Sint(i.Value + 10); return j; } }
El programa repite el test 5 veces y al final muestra los resultados. Lo ejecuté varias veces, pero el patrón es siempre muy similar. En mi máquina, compilado en Release esos fueron los resultados:
int took 254. Sint took 569. Diff is 315 (124.015748031496%) int took 244. Sint took 447. Diff is 203 (83.1967213114754%) int took 249. Sint took 436. Diff is 187 (75.1004016064257%) int took 236. Sint took 460. Diff is 224 (94.9152542372881%) int took 235. Sint took 441. Diff is 206 (87.6595744680851%) int took 251. Sint took 427. Diff is 176 (70.1195219123506%) int took 241. Sint took 446. Diff is 205 (85.0622406639004%) int took 241. Sint took 452. Diff is 211 (87.551867219917%) int took 263. Sint took 472. Diff is 209 (79.467680608365%) int took 258. Sint took 473. Diff is 215 (83.3333333333333%)
Es decir hay una pérdida de rendimiento que oscila entre el 70 y el 100% aproximadamente. Ahora ya se trata de juzgar de si es asumible.
En mi caso decidí que sí, a pesar de la pérdida de rendimiento, tampoco iba a tener un número de operaciones tan bestia (observad que estamos hablando de alrededor de perder un cuarto de segundo cada 100 millones de operaciones).
¿Y por qué hacer esto?
Esa es una buena pregunta, claro. O sea, que ventajas tiene usar una estructura de un solo campo respecto a usar una variable. Es decir, si ya tengo ints, para ¿qué narices quiero usar la estructura Sint?
Una respuesta es para dar semántica al código. Te pongo mi ejemplo concreto. Tengo una función al que se le pasa un color. Hay 8 colores predefinidos, con valores de 0 a 7, pero luego se pueden definir colores propios a los que se les asignan índices de 8 para arriba (el número de colores que se pueden definir depende de la plataforma de ejecución). Todas las funciones de pintado esperan el color que como puedes ver es un int (cuyo rango va de 0 hasta un valor máximo que no se conoce a priori).
Usar una estructura Color con un solo campo int, me permite definir una semántica propia: las funciones que reciben/devuelven un Color sé que trabajan con colores. En definitiva estoy tipando el parámetro o valor de retorno. Otra opción es que puedo usar métodos de extensión (o en la propia estructura) para agrupar operaciones que se pueden hacer con colores. Y además todas aquellas operaciones que se pueden hacer con ints y no con colores (como dividirlos p. ej.) no se pueden realizar.
En mi caso, por si te pica la curiosidad, terminé con esa estructura:
public static class TvColorNames { public const int Black = 0; public const int Red = 1; public const int Green = 2; public const int Yellow = 3; public const int Blue = 4; public const int Magenta = 5; public const int Cyan = 6; public const int White = 7; public static IEnumerable<int> AllStandardColors() => new[] { Black, Red, Green, Yellow, Blue, Magenta, Cyan, White }; public const int StandardColorsCount = 8; } public struct TvColor { public readonly int Value; public TvColor(int value) => Value = value; public TvColor Plus(int valueToAdd) => new TvColor(Value + valueToAdd); public readonly static TvColor Black = new TvColor(TvColorNames.Black); public readonly static TvColor Red = new TvColor(TvColorNames.Red); public readonly static TvColor Green = new TvColor(TvColorNames.Green); public readonly static TvColor Yellow = new TvColor(TvColorNames.Yellow); public readonly static TvColor Blue = new TvColor(TvColorNames.Blue); public readonly static TvColor Magenta = new TvColor(TvColorNames.Magenta); public readonly static TvColor Cyan = new TvColor(TvColorNames.Cyan); public readonly static TvColor White = new TvColor(TvColorNames.White); public static explicit operator short(TvColor color) => (short)color.Value; public static bool operator ==(TvColor one, TvColor other) => one.Value == other.Value; public static bool operator !=(TvColor one, TvColor other) => one.Value != other.Value; public override bool Equals(object obj) { if (obj is TvColor) return ((TvColor)obj).Value == Value; if (obj is int) return (int)obj == Value; return base.Equals(obj); } public override int GetHashCode() => Value.GetHashCode(); }
Y el código queda un poco más legible, ya que las funciones que operan con colores se definen en base a este tipo, no en base a un int como p. ej:
public interface IStyleBuilder { IStyleBuilder DesiredStandard(TvColor fore, TvColor back,CharacterAttributeModifiers attributes = CharacterAttributeModifiers.Normal); IStyleBuilder DesiredFocused(TvColor fore, TvColor back, CharacterAttributeModifiers attributes = CharacterAttributeModifiers.Normal); }
En general uso esta estructura para el mismo caso en que, si los valores fueran fijos, usaría un enum. Si solo hubiese la posibilidad de usar los 8 colores, hubiese creado un enum con esos 8 valores. Pero, al haber la posibilidad de crear más colores y de no saber a priori cuantos son, me impide usar un enum y por ello uso la estructura. Pero en ambos casos la idea es la misma: dotar el código de semántica, hacerlo más claro.
Que valga la pena o no pagar el precio de rendimiento eso ya queda a criterio de cada cual.
C# Varianza en delegados
¡Buenas! A raíz de una situación en la que me he encontrado en un proyecto real (de la que luego hablaré) me he decidido a escribir este post para comentar algunas cosillas sobre varianzas en los delegados mismos.
Cuando hablamos de varianzas en delegados hay que contemplar dos aspectos:
- Varianzas entre los tipos definidos por el delegado y los tipos de la función asignada al delegado
- Varianzas entre el tipo del delegado y otros tipos (en este caso object).
- Las combinaciones entre esos dos puntos.
Marker Interface: ¿Patrón o Anti-patrón?
Llamamos marker interface a una interfaz vacía. Sí, sí sin métodos ni propiedades ni nada. A pesar de que te pueda parecer una tontería tiene sus usos. Vamos hablar un poco de este patrón y sus usos y por qué es en cierta manera un anti-patrón, aunque no siempre, porque en esa vida, como todo, todo depende…
Hoy he echado en falta poder definir macros en C#
Pues sí… y no me avergüenzo de decirlo, si hoy C# tuviese algo como el preprocesador de C/C++ me hubiese hecho feliz.
Continúa leyendo Hoy he echado en falta poder definir macros en C#
C# 7–Default method implementation?
El otro día estuve hablando con Joan Jané, sobre la funcionalidad que se está valorando para C#7 o (probablemente, dado su estado) más adelante. A falta de un nombre mejor llamaré a esa funcionalidad “Default method implementation” porque así se conoce en Java, donde esa funcionalidad ya existe.
Joan y yo teníamos puntos de vista totalmente opuestos a dicha característica, mientras que para mi era un añadido muy interesante al lenguaje, Joan se alineaba más con las tesis que Fernando Escolar expone en un post en su blog. Para Fer, esa feature es la peor idea que ha tenido Java en los últimos años. A mi me da la sensación que verlo así es no entender exactamente que añade esa característica y analizarla desde una posición demasiado rígida. Joan argumentaba problemas relacionados con SOLID, generalmente con el SRP y con la segregación de interfaces. En este post voy a comentar lo que, desde mi punto de vista, permitiría esa característica de estar disponible. Y por qué, no solo no es una mala idea, si no, a priori, todo lo contrario (tanto en Java como en C# por más que se empecine Fer en decir lo contrario).
Algunas consideraciones sobre las structs
El otro día un tweet de Juan Quijano, animó una pequeña discusión sobre la diferencia entre clases y estructuras en .NET. Este no es el primer post que escribo al respecto, pero bueno, aprovechando la coyuntura vamos a comentar algunas de las cosas que se mencionaron en el pequeño debate que generó el tweet de Juan.