Cómo migrar mi pensamiento de C ++ a C #

12

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 usingen 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.
Andrés
fuente
# 1 es difícil para mí, me gustaría saber la respuesta por mí mismo. # 2 - ¿podría dar un ejemplo simple de código C ++ y explicar por qué lo necesita? Para el n. ° 3: use C#para generar otro código. Puede leer una CSVo una XMLo qué archivo tiene como entrada y generar una C#o un SQLarchivo. Esto puede ser más poderoso que usar macros funcionales.
Trabajo
Sobre el tema de la funcionalidad tipo macro, ¿qué cosas específicas estás tratando de hacer? Lo que he encontrado es que generalmente hay una construcción de lenguaje C # para manejar lo que hubiera hecho con una macro en C ++. Consulte también las directivas de preprocesador de C #: msdn.microsoft.com/en-us/library/4y6tbswk.aspx
ravibhagw
Muchas cosas hechas con macros en C ++ se pueden hacer con reflejo en C #.
Sebastian Redl
Es una de mis manías cuando alguien dice "La herramienta adecuada para el trabajo correcto: elige un idioma en función de lo que vas a necesitar". Para programas grandes en lenguajes de propósito general, esta es una declaración inútil e inútil. Dicho esto, lo que C ++ es mejor que casi cualquier otro lenguaje es la gestión de recursos determinista. ¿Es la gestión de recursos determinista una característica principal de su programa o algo a lo que está acostumbrado? Si no es una característica importante del usuario, puede estar demasiado concentrado en eso.
Ben

Respuestas:

16

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.

¿Cómo obtener un diseño perfecto de C ++ que no parezca tonto en C #?

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.

Telastyn
fuente
Con respecto a la gestión de recursos, en C # es bastante común tener clases de administrador (que pueden ser estáticas o dinámicas).
rwong
Con respecto a los recursos compartidos: los administrados son fáciles en C # (si las condiciones de carrera son irrelevantes), los no administrados son mucho más desagradables.
Deduplicador
@rwong: común, pero las clases de administrador siguen siendo un antipatrón a evitar.
Telastyn
Con respecto a la administración de la memoria, descubrí que cambiar a C # lo hacía más difícil, especialmente al principio. Creo que debes ser más consciente que en C ++. En C ++, usted sabe exactamente cuándo y dónde se asigna, desasigna y hace referencia a cada objeto. En C #, todo esto está oculto para ti. Muchas veces en C # ni siquiera se da cuenta de que se ha obtenido una referencia a un objeto y la memoria comienza a perder. Diviértete rastreando pérdidas de memoria en C #, que generalmente son triviales para descubrir en C ++. Por supuesto, con experiencia aprende estos matices de C #, por lo que tienden a ser menos problemáticos.
Dunk
4

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.

  • Use IDisposablepara hacer una limpieza determinista si es necesario.
  • Cuando necesite transferir la propiedad de un IDisposable, solo asegúrese de que la API involucrada sea clara en esto y no lo use usingen este caso, porque usingno 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.

Martin Ba
fuente
2

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 IDisposableinterfaz, utilizada de la siguiente manera:

using (FileStream fs = File.OpenRead(@"c:\path\filename.txt"))
{
   //Do stuff with the file.
} //File will be closed here.

No debe esperar implementarlo con IDisposablefrecuencia (y hacerlo es un poco avanzado), ya que no es necesario para los recursos administrados. Sin embargo, debe usar la usingconstrucción (o llamarse a Disposesí mismo, si no usinges factible) para cualquier implementación IDisposable. 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).

Brian
fuente
RAII es mi mayor problema de hecho. Digamos que tengo un recurso (digamos un archivo) creado por un hilo y regalado a otro. ¿Cómo puedo asegurarme de que se elimine en un momento determinado cuando ese hilo ya no lo necesita? Por supuesto, podría llamar a Dispose pero luego se ve mucho más feo que los punteros compartidos en C ++. No hay nada de RAII en él.
Andrew
@ Andrew Lo que describe parece implicar la transferencia de la propiedad, por lo que ahora el segundo hilo es responsable Disposing()del archivo y probablemente debería usarlo using.
svick
@ Andrew - En general, no deberías hacer eso. En su lugar, debe decirle al hilo cómo obtener el archivo y dejar que tenga la propiedad de toda la vida.
Telastyn
1
@Telastyn ¿Significa que ceder la propiedad de un recurso administrado es un problema mayor en C # que en C ++? Muy sorprendente
Andrew
66
@Andrew: Permítanme resumir: en C ++, obtiene una administración uniforme y semiautomática de todos los recursos. En C #, obtiene una administración de memoria completamente automática, y una administración completamente manual de todo lo demás. No hay un cambio real, por lo que su única pregunta real no es "¿cómo soluciono esto?", Solo "¿cómo me acostumbro a esto?"
Jerry Coffin
2

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 un foreachbucle.

Tus ejemplos específicos:

Controlar la vida útil de los recursos que requieren limpieza determinista (como archivos). Esto es fácil de usar, 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.

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 Disposepatrón para lidiar con la limpieza.

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?

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 un List<int>o un List<string>? Todas las operaciones en List<T>son genéricas para aplicar a cualquiera List<T> . Si desea cambiar el comportamiento del .Addmétodo en a List<string>, cree una clase derivada MySpecializedStringCollection : List<string>o una clase compuesta MySpecializedStringCollection : 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.

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 (condición) {...} repetitivo y todavía no es igual en términos de desencadenar efectos secundarios.

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.

Stephen
fuente
3
WRT. transferencia de recursos: "nunca deberías hacer esto" no es suficiente en mi humilde opinión. A menudo, un diseño muy bueno sugiere la transferencia de un IDisposable( no el recurso en bruto) de una clase a otra: solo vea la API de StreamWriter, donde esto tiene perfecto sentido para Dispose()la secuencia pasada. Y ahí está el agujero en el lenguaje C #: no se puede expresar esto en el sistema de tipos.
Martin Ba
2
"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". Esta es una mala idea en C ++ también. La abstracción de costo cero es el gran poder de C ++, y foreach u otros algoritmos no deberían ser más lentos que un bucle for escrito a mano en la mayoría de los casos.
Sebastian Redl
1

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í!

gbjbaanb
fuente
1
Pasar un objeto a una rutina para compartir datos subyacentes está perfectamente bien en C #, al menos desde una perspectiva de sintaxis.
Brian