Soy un desarrollador experimentado de C ++, conozco el lenguaje con gran detalle y he usado algunas de sus características específicas de manera intensiva. Además, conozco los principios de OOD y los patrones de diseño. Ahora estoy aprendiendo C # pero no puedo evitar la sensación de no poder deshacerme de la mentalidad de C ++. Me até tan fuerte a las fortalezas de C ++ que no puedo vivir sin algunas de las características. Y no puedo encontrar ninguna buena solución o reemplazo para ellos en C #.
¿Qué buenas prácticas , patrones de diseño , modismos que son diferentes en C # desde la perspectiva de C ++ pueden sugerir? ¿Cómo obtener un diseño perfecto de C ++ que no parezca tonto en C #?
Específicamente, no puedo encontrar una buena manera de tratar con C # (ejemplos recientes):
- Controlar la vida útil de los recursos que requieren limpieza determinista (como archivos). Es fácil tenerlo
using
en la mano, pero ¿cómo usarlo correctamente cuando se transfiere la propiedad del recurso [... entre hilos]? En C ++, simplemente usaría punteros compartidos y dejaría que se encargue de la 'recolección de basura' en el momento adecuado. - Constante luchando con funciones primordiales para genéricos específicos (me encantan cosas como la especialización parcial de plantilla en C ++). ¿Debo abandonar cualquier intento de hacer una programación genérica en C #? ¿Quizás los genéricos son limitados a propósito y no es C # -ish usarlos excepto para un dominio específico de problemas?
- Funcionalidad tipo macro. Si bien generalmente es una mala idea, para algunos dominios de problemas no hay otra solución alternativa (por ejemplo, evaluación condicional de una declaración, como con los registros que solo deberían ir a las versiones de depuración). No tenerlos significa que necesito poner más
if (condition) {...}
repetitivo y todavía no es igual en términos de desencadenar efectos secundarios.
C#
para generar otro código. Puede leer unaCSV
o unaXML
o qué archivo tiene como entrada y generar unaC#
o unSQL
archivo. Esto puede ser más poderoso que usar macros funcionales.Respuestas:
En mi experiencia, no hay libro para hacer esto. Los foros ayudan con modismos y mejores prácticas, pero al final tendrás que dedicarle tiempo. Practica resolviendo varios problemas diferentes en C #. Mira lo que fue difícil / incómodo e intenta hacerlos menos difíciles y menos incómodos la próxima vez. Para mí, este proceso tardó aproximadamente un mes antes de que "lo entendiera".
Lo más importante es centrarse en la falta de administración de memoria. En C ++ esto domina cada decisión de diseño que usted toma, pero en C # es contraproducente. En C #, a menudo desea ignorar la muerte u otras transiciones de estado de sus objetos. Ese tipo de asociación agrega un acoplamiento innecesario.
En su mayoría, concéntrese en usar las nuevas herramientas de la manera más efectiva, no en golpearse por un apego a las antiguas.
Al darse cuenta de que diferentes modismos empujan hacia diferentes enfoques de diseño. No puede simplemente transferir algo 1: 1 entre (la mayoría) idiomas y esperar algo hermoso e idiomático en el otro extremo.
Pero en respuesta a escenarios específicos:
Recursos no administrados compartidos : esta fue una mala idea en C ++, y es una mala idea en C #. Si tiene un archivo, ábralo, úselo y ciérrelo. Compartirlo entre subprocesos solo es un problema, y si solo un subproceso realmente lo está usando, haga que ese subproceso lo abra / propio / lo cierre. Si realmente tiene un archivo de larga duración por alguna razón, cree una clase para representarlo y deje que la aplicación lo administre según sea necesario (cierre antes de salir, empate cerca del tipo de eventos de Cierre de aplicaciones, etc.)
Especialización de plantilla parcial : no tiene suerte aquí, aunque cuanto más he usado C #, más encuentro el uso de plantilla que hice en C ++ para caer en YAGNI. ¿Por qué hacer algo genérico cuando T es siempre un int? Otros escenarios se resuelven mejor en C # mediante la aplicación liberal de interfaces. No necesita genéricos cuando una interfaz o un tipo de delegado pueden escribir fuertemente lo que desea variar.
Preprocesadores condicionales : C # tiene
#if
, se vuelven locos. Mejor aún, tiene atributos condicionales que simplemente eliminarán esa función del código si no se define un símbolo del compilador.fuente
Hasta donde puedo ver, lo único que permite la gestión determinista de toda la vida es
IDisposable
, que se descompone (como en: sin soporte de idioma o sistema de tipos) cuando, como notaron, necesitan transferir la propiedad de dicho recurso.Estar en el mismo barco que usted: desarrollador de C ++ haciendo C # atm. - la falta de soporte para modelar la transferencia de propiedad (archivos, conexiones db, ...) en los tipos / firmas de C # me ha resultado muy molesta, pero tengo que admitir que funciona bastante bien "simplemente" confiar en convenciones y documentos de API para hacer esto bien.
IDisposable
para hacer una limpieza determinista si es necesario.IDisposable
, solo asegúrese de que la API involucrada sea clara en esto y no lo useusing
en este caso, porqueusing
no se puede "cancelar", por así decirlo.Sí, no es tan elegante como lo que puedes expresar en el sistema de tipos con C ++ moderno (mover las estadísticas, etc.), pero hace el trabajo y (sorprendentemente para mí) la comunidad C # en su conjunto no parece hacerlo. cuidado en exceso, AFAICT.
fuente
Usted mencionó dos características específicas de C ++. Discutiré RAII:
Por lo general, no es aplicable, ya que C # es basura recolectada. La construcción de C # más cercana es probablemente la
IDisposable
interfaz, utilizada de la siguiente manera:No debe esperar implementarlo con
IDisposable
frecuencia (y hacerlo es un poco avanzado), ya que no es necesario para los recursos administrados. Sin embargo, debe usar lausing
construcción (o llamarse aDispose
sí mismo, si nousing
es factible) para cualquier implementaciónIDisposable
. Incluso si arruina esto, las clases de C # bien diseñadas tratarán de manejar esto por usted (usando finalizadores). Sin embargo, en un lenguaje de recolección de basura, el patrón RAII no funciona completamente (ya que la recolección no es determinista), por lo que la limpieza de sus recursos se retrasará (a veces catastróficamente).fuente
Disposing()
del archivo y probablemente debería usarlousing
.Esta es una muy buena pregunta, ya que he visto a varios desarrolladores de C ++ escribir un terrible código C #.
Es mejor no pensar en C # como "C ++ con una sintaxis más agradable". Son diferentes idiomas que requieren diferentes enfoques en su pensamiento. C ++ te obliga a pensar constantemente en lo que harán la CPU y la memoria. C # no es así. C # está específicamente diseñado para que no esté pensando en la CPU y la memoria y, en cambio, esté pensando en el dominio comercial para el que está escribiendo .
Un ejemplo de esto que he visto es que a muchos desarrolladores de C ++ les gustará usar bucles porque son más rápidos que foreach. Esto suele ser una mala idea en C # porque restringe los posibles tipos de la colección que se repite (y, por lo tanto, la reutilización y flexibilidad del código).
Creo que la mejor manera de reajustar de C ++ a C # es intentar abordar la codificación desde una perspectiva diferente. Al principio, esto será difícil, ya que a lo largo de los años estará completamente acostumbrado a usar el hilo "qué están haciendo la CPU y la memoria" en su cerebro para filtrar el código que escribe. Pero en C #, debería pensar en las relaciones entre los objetos en el dominio empresarial. "Qué quiero hacer" en lugar de "qué está haciendo la computadora".
Si desea hacer algo a todo en una lista de objetos, en lugar de escribir un bucle for en la lista, cree un método que tome un
IEnumerable<MyObject>
y use unforeach
bucle.Tus ejemplos específicos:
Nunca debe hacer esto en C # (particularmente entre hilos). Si necesita hacer algo en un archivo, hágalo en un lugar a la vez. Está bien (y, de hecho, es una buena práctica) escribir una clase de contenedor que administre el recurso no administrado que se pasa entre sus clases, pero no intente pasar un archivo entre subprocesos y haga que las clases separadas lo escriban / lean / cierren / abran. No comparta la propiedad de los recursos no administrados. Use el
Dispose
patrón para lidiar con la limpieza.Los genéricos en C # están diseñados para ser genéricos. Las especializaciones de genéricos deben ser manejadas por clases derivadas. ¿Por qué debería un
List<T>
comportamiento diferente si es unList<int>
o unList<string>
? Todas las operaciones enList<T>
son genéricas para aplicar a cualquieraList<T>
. Si desea cambiar el comportamiento del.Add
método en aList<string>
, cree una clase derivadaMySpecializedStringCollection : List<string>
o una clase compuestaMySpecializedStringCollection : IList<string>
que utilice el genérico internamente pero que haga las cosas de manera diferente. Esto te ayuda a evitar violar el Principio de sustitución de Liskov y atornillar a los demás que usan tu clase.Como han dicho otros, puede usar comandos de preprocesador para hacer esto. Los atributos son aún mejores. En general, los atributos son la mejor manera de manejar cosas que no son funcionalidades "centrales" para una clase.
En resumen, al escribir C #, tenga en cuenta que debe pensar por completo en el dominio empresarial y no en la CPU y la memoria. Puede optimizar más adelante si es necesario , pero su código debe reflejar las relaciones entre los principios comerciales que está tratando de mapear.
fuente
IDisposable
( no el recurso en bruto) de una clase a otra: solo vea la API de StreamWriter, donde esto tiene perfecto sentido paraDispose()
la secuencia pasada. Y ahí está el agujero en el lenguaje C #: no se puede expresar esto en el sistema de tipos.Lo que fue más diferente para mí fue la tendencia a renovar todo . Si quieres un objeto, olvida intentar pasarlo a una rutina y compartir los datos subyacentes, parece que el patrón C # es solo para crear un nuevo objeto. Esto, en general, parece ser mucho más de la manera C #. Al principio, este patrón parecía muy ineficiente, pero supongo que crear la mayoría de los objetos en C # es una operación rápida.
Sin embargo, también hay clases estáticas que realmente me molestan, ya que de vez en cuando intentas crear una solo para descubrir que solo tienes que usarla. Creo que no es tan fácil saber cuál usar.
Estoy muy contento en un programa de C # de simplemente ignorarlo cuando ya no lo necesito, pero solo recuerda lo que contiene ese objeto; en general, solo tienes que pensar si tiene algunos datos 'inusuales', los objetos que solo consisten en memoria simplemente se van por sí mismos, los demás tendrán que ser eliminados o tendrá que ver cómo destruirlos, de forma manual o mediante un bloque de uso, si no.
sin embargo, lo recogerá muy rápidamente, C # no es muy diferente de VB, por lo que puede tratarlo como la misma herramienta RAD que es (si ayuda a pensar en C # como VB.NET, entonces, la sintaxis es solo un poco diferente, y mis viejos días de usar VB me llevaron a la mentalidad correcta para el desarrollo de RAD). Lo único difícil es determinar qué parte de las enormes bibliotecas .net debe usar. ¡Google es definitivamente tu amigo aquí!
fuente