Como migrar meu pensamento de C ++ para C #

12

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 usingem 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.
Andrew
fonte
# 1 é difícil para mim, gostaria de saber a resposta. # 2 - você poderia dar um exemplo simples de código C ++ e explicar por que você precisa disso? Para # 3 - use C#para gerar outro código. Você pode ler um CSVou um XMLou o que você arquivo como entrada e gerar um C#ou um SQLarquivo. Isso pode ser mais poderoso do que usar macros funcionais.
Job
No tópico da funcionalidade semelhante a macro, que coisas específicas você está tentando fazer? O que eu descobri é que geralmente há uma construção de linguagem C # para lidar com o que eu teria feito com uma macro em C ++. Verifique também as diretivas de pré-processador de C #: msdn.microsoft.com/en-us/library/4y6tbswk.aspx
ravibhagw
Muitas coisas feitas com macros em C ++ podem ser feitas com reflexão em C #.
Sebastian Redl
É uma das minhas irritações quando alguém diz "A ferramenta certa para o trabalho certo - escolha um idioma com base no que você precisará". Para programas grandes em linguagens de uso geral, essa é uma declaração inútil e inútil. Dito isto, o melhor em C ++ do que quase qualquer outra linguagem é o gerenciamento determinístico de recursos. O gerenciamento determinístico de recursos é um recurso principal do seu programa ou algo a que você está acostumado? Se não é um recurso importante para o usuário, você pode se concentrar demais nisso.
Ben

Respostas:

16

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.

Como obter um design C ++ perfeito não parecendo idiota em C #?

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 #iflouco. Melhor ainda, possui atributos condicionais que simplesmente eliminam essa função do código se um símbolo do compilador não estiver definido.

Telastyn
fonte
Em relação ao gerenciamento de recursos, em C # é bastante comum ter classes de gerenciador (que podem ser estáticas ou dinâmicas).
rwong
Com relação aos recursos compartilhados: Os gerenciados são fáceis em C # (se as condições de corrida são irrelevantes), os não gerenciados são muito mais desagradáveis.
Deduplicator
@rwong - comum, mas as classes de gerente ainda são um anti-padrão a ser evitado.
Telastyn
Com relação ao gerenciamento de memória, descobri que a mudança para C # tornava isso mais difícil, especialmente no começo. Eu acho que você precisa estar mais consciente do que em C ++. No C ++, você sabe exatamente quando e onde cada objeto é alocado, desalocado e referenciado. Em C #, tudo isso está oculto para você. Muitas vezes em C # você nem percebe que uma referência a um objeto foi obtida e a memória começa a vazar. Divirta-se rastreando vazamentos de memória em C #, que geralmente são triviais para descobrir em C ++. Obviamente, com a experiência, você aprende essas nuances do C # para que elas se tornem menos problemáticas.
Dunk
4

Até onde eu posso ver, a única coisa que permite o Gerenciamento determinístico da vida útil é IDisposableque 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.

  • Use IDisposablepara fazer uma limpeza determinística, se necessário.
  • Quando você precisar transferir a propriedade de um IDisposable, certifique-se de que a API envolvida seja clara e não use usingneste caso, porque usingnã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.

Martin Ba
fonte
2

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 IDisposableinterface, usada da seguinte maneira:

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

Você não deve implementar com IDisposablefrequência (e isso é um pouco avançado), pois não é necessário para os recursos gerenciados. No entanto, você deve usar a usingconstrução (ou chamar a Disposesi mesmo, se usingfor inviável) para qualquer implementação IDisposable. 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).

Brian
fonte
RAII é o meu maior problema, de fato. Digamos que eu tenha um recurso (digamos um arquivo) criado por um encadeamento e entregue a outro. Como posso garantir que ele seja descartado em um determinado momento, quando esse segmento não precisar mais dele? É claro que eu poderia chamar Dispose, mas parece muito mais feio do que ponteiros compartilhados em C ++. Não há nada de RAII nele.
Andrew
@ Andrew O que você descreve parece implicar transferência de propriedade, agora o segundo segmento é responsável pelo Disposing()arquivo e provavelmente deve usá-lo using.
precisa
@ Andrew - Em geral, você não deve fazer isso. Em vez disso, você deve informar ao thread como obter o arquivo e permitir que ele seja dono de toda a vida útil.
Telastyn
1
@Telastyn Isso significa que abrir mão da propriedade sobre um recurso gerenciado é um problema maior em C # do que em C ++? Bastante surpreendente.
Andrew
6
@ Andrew: Deixe-me resumir: em C ++, você obtém um gerenciamento uniforme e semi-automático de todos os recursos. No C #, você obtém um gerenciamento completamente automático da memória - e um gerenciamento completamente manual de tudo o mais. Não há como mudar isso de verdade, então sua única pergunta real não é "como faço para corrigir isso?", Apenas "como me acostumo a isso?"
Jerry Coffin
2

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 um foreachloop.

Seus exemplos específicos:

Controlando a vida útil dos recursos que requerem limpeza determinística (como arquivos). É fácil usá-lo 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.

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 Disposepadrão para lidar com a limpeza.

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?

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 é um List<int>ou um List<string>? Todas as operações List<T>são genéricas, de modo a serem aplicadas a qualquer uma List<T> . Se você deseja alterar o comportamento do .Addmétodo em a List<string>, crie uma classe derivada MySpecializedStringCollection : List<string>ou uma classe composta MySpecializedStringCollection : 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.

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 se (condição) {...} clichê e ainda não é igual em termos de desencadear efeitos colaterais.

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.

Stephen
fonte
3
WRT. transferência de recursos: "você nunca deve fazer isso" não impede IMHO. Muitas vezes, um design muito bom sugere a transferência de um IDisposable( não o recurso bruto) de uma classe para outra: basta ver a API StreamWriter, onde isso faz todo sentido para Dispose()o fluxo passado. E aí está o buraco na linguagem C # - você não pode expressar isso no sistema de tipos.
Martin Ba
2
"Um exemplo disso que eu vi é que muitos desenvolvedores de C ++ gostariam de usar loops porque são mais rápidos que o foreach". Essa também é uma má idéia em C ++. A abstração de custo zero é o grande poder do C ++, e o foreach ou outros algoritmos não devem ser mais lentos do que um loop for escrito à mão na maioria dos casos.
Sebastian Redl
1

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!

gbjbaanb
fonte
1
Passar um objeto para uma rotina para compartilhar dados subjacentes é perfeitamente adequado em C #, pelo menos da perspectiva da sintaxe.
22413 Brian