Sou um desenvolvedor experiente em C ++, conheço a linguagem em grandes detalhes e usei intensamente alguns de seus recursos específicos. Além disso, conheço princípios de OOD e padrões de design. Agora estou aprendendo C #, mas não consigo parar de sentir que não consigo me livrar da mentalidade de C ++. Eu me amarrei tanto aos pontos fortes do C ++ que não posso viver sem alguns dos recursos. E não consigo encontrar boas soluções ou substituições para eles em C #.
Que boas práticas , padrões de design , idiomas diferentes em C # da perspectiva do C ++ podem ser sugeridos? Como obter um design perfeito em C ++ sem parecer idiota em c #?
Especificamente, não consigo encontrar uma boa maneira de lidar com C # (exemplos recentes):
- Controlando a vida útil dos recursos que requerem limpeza determinística (como arquivos). É fácil ter isso
using
em mãos, mas como usá-lo corretamente quando a propriedade do recurso está sendo transferida [... entre os threads]? Em C ++, eu simplesmente usava ponteiros compartilhados e deixava cuidar da 'coleta de lixo' na hora certa. - Luta constante com funções de substituição de genéricos específicos (eu amo coisas como especialização parcial de modelos em C ++). Devo abandonar todas as tentativas de executar qualquer programação genérica em c #? Talvez os genéricos sejam limitados de propósito e não seja c # -ish usá-los, exceto por um domínio específico de problemas?
- Funcionalidade semelhante a macro. Embora geralmente seja uma péssima idéia, para alguns domínios de problemas, não há outra solução alternativa (por exemplo, avaliação condicional de uma instrução, como nos logs que devem apenas ir para as versões de depuração). Não tê-los significa que eu preciso colocar mais
if (condition) {...}
clichê e ainda não é igual em termos de desencadear efeitos colaterais.
C#
para gerar outro código. Você pode ler umCSV
ou umXML
ou o que você arquivo como entrada e gerar umC#
ou umSQL
arquivo. Isso pode ser mais poderoso do que usar macros funcionais.Respostas:
Na minha experiência, não há livro para fazer isso. Os fóruns ajudam com expressões idiomáticas e práticas recomendadas, mas no final você precisará dedicar tempo. Pratique resolvendo vários problemas diferentes em C #. Veja o que foi difícil / embaraçoso e tente torná-los menos difíceis e menos embaraçosos na próxima vez. Para mim, esse processo levou cerca de um mês antes que eu "entendesse".
A principal coisa a se concentrar é a falta de gerenciamento de memória. Em C ++, isso domina todas as decisões de design que você toma, mas em C # é contraproducente. Em C #, muitas vezes você deseja ignorar a morte ou outras transições de estado de seus objetos. Esse tipo de associação adiciona acoplamentos desnecessários.
Principalmente, concentre-se em usar as novas ferramentas com mais eficiência, sem se atrapalhar com o apego às antigas.
Percebendo que idiomas diferentes avançam para diferentes abordagens de design. Você não pode simplesmente portar algo 1: 1 entre (a maioria) dos idiomas e esperar algo bonito e idiomático do outro lado.
Mas em resposta a cenários específicos:
Recursos não gerenciados compartilhados - Essa foi uma péssima idéia em C ++ e é uma péssima idéia em C #. Se você tiver um arquivo, abra-o, use-o e feche-o. Compartilhá-lo entre os segmentos está apenas pedindo problemas, e se apenas um segmento estiver realmente usando, deixe esse segmento abrir / possuir / fechá-lo. Se você realmente tem um arquivo de longa duração por algum motivo, faça uma classe para representá-lo e permita que o aplicativo gerencie isso conforme necessário (feche antes da saída, vincule ao tipo de eventos de Fechamento de aplicativo, etc.)
Especialização parcial de modelos - Você está sem sorte aqui, embora quanto mais eu usei C #, mais eu acho que o uso de modelos que fiz em C ++ se enquadra no YAGNI. Por que tornar algo genérico quando T é sempre um int? Outros cenários são melhor resolvidos em C # pela aplicação liberal de interfaces. Você não precisa de genéricos quando um tipo de interface ou delegado pode digitar fortemente o que você deseja variar.
Condicionais do pré-processador - C # ficou
#if
louco. Melhor ainda, possui atributos condicionais que simplesmente eliminam essa função do código se um símbolo do compilador não estiver definido.fonte
Até onde eu posso ver, a única coisa que permite o Gerenciamento determinístico da vida útil é
IDisposable
que se decompõe (como em: nenhum idioma ou tipo de suporte do sistema) quando, como você notou, precisa transferir a propriedade de um recurso.Estar no mesmo barco que você - desenvolvedor C ++ fazendo C # atm. - a falta de suporte para modelar a transferência de propriedade (arquivos, conexões db, ...) nos tipos / assinaturas de C # tem sido muito chata para mim, mas devo admitir que funciona muito bem "apenas" confiar nos documentos de convenções e API para fazer isso direito.
IDisposable
para fazer uma limpeza determinística, se necessário.IDisposable
, certifique-se de que a API envolvida seja clara e não useusing
neste caso, porqueusing
não pode ser "cancelado" por assim dizer.Sim, não é tão elegante quanto o que você pode expressar no sistema de tipos com C ++ moderno (semântica de movimentos, etc.), mas faz o trabalho e (surpreendentemente para mim) a comunidade C # como um todo não parece cuidados excessivos, AFAICT.
fonte
Você mencionou dois recursos específicos do C ++. Vou discutir RAII:
Geralmente não é aplicável, pois o C # é coletado de lixo. A construção C # mais próxima é provavelmente a
IDisposable
interface, usada da seguinte maneira:Você não deve implementar com
IDisposable
frequência (e isso é um pouco avançado), pois não é necessário para os recursos gerenciados. No entanto, você deve usar ausing
construção (ou chamar aDispose
si mesmo, seusing
for inviável) para qualquer implementaçãoIDisposable
. Mesmo que você estrague tudo, classes C # bem projetadas tentarão lidar com isso para você (usando finalizadores). No entanto, em um idioma coletado pelo lixo, o padrão RAII não funciona completamente (já que a coleta não é determinística), portanto, a limpeza dos recursos será atrasada (às vezes catastroficamente).fonte
Disposing()
arquivo e provavelmente deve usá-lousing
.Essa é uma pergunta muito boa, pois já vi vários desenvolvedores de C ++ escreverem códigos C # terríveis.
É melhor não pensar em C # como "C ++ com sintaxe melhor". São linguagens diferentes que requerem abordagens diferentes em seu pensamento. O C ++ obriga você a pensar constantemente sobre o que a CPU e a memória farão. C # não é assim. O C # foi especificamente projetado para que você não esteja pensando na CPU e na memória e, em vez disso, no domínio comercial para o qual está escrevendo .
Um exemplo disso que eu vi é que muitos desenvolvedores de C ++ gostariam de usar loops porque são mais rápidos que o foreach. Geralmente, é uma má idéia no C # porque restringe os tipos possíveis de coleção que estão sendo iterados (e, portanto, a reutilização e flexibilidade do código).
Eu acho que a melhor maneira de reajustar de C ++ para C # é tentar abordar a codificação de uma perspectiva diferente. No início, isso será difícil, porque ao longo dos anos você estará acostumado a usar o segmento "o que a CPU e a memória estão fazendo" em seu cérebro para filtrar o código que você escreve. Mas em C #, você deve pensar sobre os relacionamentos entre os objetos no domínio comercial. "O que eu quero fazer" em oposição a "o que o computador está fazendo".
Se você quiser fazer algo em tudo na lista de objetos, em vez de escrever um loop for na lista, crie um método que
IEnumerable<MyObject>
use e use umforeach
loop.Seus exemplos específicos:
Você nunca deve fazer isso em C # (principalmente entre threads). Se você precisar fazer algo em um arquivo, faça-o em um local ao mesmo tempo. Não há problema (e, de fato, é uma boa prática) escrever uma classe de wrapper que gerencia o recurso não gerenciado que é passado entre suas classes, mas não tente passar um Arquivo entre threads e tenha classes separadas para gravar / ler / fechar / abrir. Não compartilhe a propriedade de recursos não gerenciados. Use o
Dispose
padrão para lidar com a limpeza.Os genéricos em C # foram projetados para serem genéricos. Especializações de genéricos devem ser tratadas por classes derivadas. Por que um
List<T>
comportamento deve ser diferente se é umList<int>
ou umList<string>
? Todas as operaçõesList<T>
são genéricas, de modo a serem aplicadas a qualquer umaList<T>
. Se você deseja alterar o comportamento do.Add
método em aList<string>
, crie uma classe derivadaMySpecializedStringCollection : List<string>
ou uma classe compostaMySpecializedStringCollection : IList<string>
que use o genérico internamente, mas faça as coisas de maneira diferente. Isso ajuda você a evitar violar o Princípio da Substituição de Liskov e ferir os outros que usam sua classe.Como já foi dito, você pode usar comandos de pré-processador para fazer isso. Os atributos são ainda melhores. Em geral, os atributos são a melhor maneira de lidar com coisas que não são a funcionalidade "principal" de uma classe.
Em resumo, ao escrever C #, lembre-se de que você deve pensar inteiramente no domínio comercial e não na CPU e na memória. Você pode otimizar mais tarde, se necessário , mas seu código deve refletir os relacionamentos entre os princípios de negócios que você está tentando mapear.
fonte
IDisposable
( não o recurso bruto) de uma classe para outra: basta ver a API StreamWriter, onde isso faz todo sentido paraDispose()
o fluxo passado. E aí está o buraco na linguagem C # - você não pode expressar isso no sistema de tipos.O que foi mais diferente para mim foi a tendência de tudo novo . Se você deseja um objeto, esqueça de passá-lo para uma rotina e compartilhar os dados subjacentes, parece que o padrão C # é apenas para criar um novo objeto para você. Isso, em geral, parece ser muito mais parecido com o C #. No início, esse padrão parecia muito ineficiente, mas acho que criar a maioria dos objetos em C # é uma operação rápida.
No entanto, há também as classes estáticas que realmente me incomodam, pois de vez em quando você tenta criar uma apenas para descobrir que precisa usá-la. Acho que não é tão fácil saber qual usar.
Estou muito feliz em um programa em C # simplesmente ignorá-lo quando não preciso mais dele, mas lembre-se do que esse objeto contém - geralmente você só precisa pensar se ele tiver alguns dados 'incomuns', objetos que consistem apenas em memória basta ir embora sozinho, os outros terão que ser descartados ou você terá que ver como destruí-los - manualmente ou usando um bloco de uso, se não.
você vai buscá-lo rapidamente, mas o C # dificilmente é diferente do VB, então você pode tratá-lo como a mesma ferramenta RAD que é (se ajudar a pensar em C # como o VB.NET, faça isso, a sintaxe é apenas um pouco diferente, e meus velhos tempos de uso do VB me levaram à mentalidade certa para o desenvolvimento do RAD). A única parte difícil é determinar qual parte das enormes bibliotecas .net você deve usar. O Google é definitivamente seu amigo aqui!
fonte