Embora seja uma pergunta geral, meu escopo é bastante C #, pois sei que linguagens como C ++ têm semânticas diferentes em relação à execução de construtores, gerenciamento de memória, comportamento indefinido etc.
Alguém me fez uma pergunta interessante que para mim não foi facilmente respondida.
Por que (ou é mesmo?) Considerado um projeto ruim para permitir que um construtor de uma classe inicie um loop sem fim (ou seja, loop de jogo)?
Existem alguns conceitos que são quebrados por isso:
- como o princípio de menor espanto, o usuário não espera que o construtor se comporte dessa maneira.
- Os testes de unidade são mais difíceis, pois você não pode criar ou injetar essa classe, pois ela nunca sai do loop.
- O final do loop (final do jogo) é então conceitualmente o tempo em que o construtor termina, o que também é estranho.
- Tecnicamente, essa classe não tem membros públicos, exceto o construtor, o que dificulta a compreensão (especialmente para idiomas em que nenhuma implementação está disponível)
E depois há problemas técnicos:
- O construtor realmente nunca termina, então o que acontece com o GC aqui? Esse objeto já está na geração 0?
- Derivar de tal classe é impossível ou pelo menos muito complicado devido ao fato de o construtor base nunca retornar
Existe algo mais obviamente ruim ou desonesto com essa abordagem?
c#
architecture
Samuel
fonte
fonte
var g = new Game {...}; g.MainLoop();
while(true)
loop em um configurador de propriedadesnew Game().RunNow = true
:?Respostas:
Qual é o propósito de um construtor? Retorna um objeto recém-construído. O que faz um loop infinito? Isso nunca volta. Como o construtor pode retornar um objeto recém-construído, se não retornar? Não pode.
Logo, um loop infinito quebra o contrato fundamental de um construtor: construir algo.
fonte
Sim, claro. É desnecessário, inesperado, inútil, deselegante. Ele viola os conceitos modernos de design de classe (coesão, acoplamento). Ele quebra o contrato do método (um construtor tem um trabalho definido e não é apenas um método aleatório). Certamente não é bem de manutenção, os futuros programadores passarão muito tempo tentando entender o que está acontecendo e tentando adivinhar os motivos pelos quais isso foi feito.
Nada disso é um "bug" no sentido de que seu código não funciona. Mas, provavelmente, incorrerá em custos secundários enormes (em relação ao custo de escrever o código inicialmente) a longo prazo, tornando o código mais difícil de manter (ou seja, testes difíceis de adicionar, difíceis de reutilizar, difíceis de depurar, difíceis de estender etc.) .).
Muitas / mais modernas melhorias nos métodos de desenvolvimento de software são feitas especificamente para facilitar o processo real de gravação / teste / depuração / manutenção de software. Tudo isso é contornado por coisas como esta, onde o código é colocado aleatoriamente porque "funciona".
Infelizmente, você encontrará regularmente programadores que ignoram completamente tudo isso. Funciona, é isso.
Para terminar com uma analogia (outra linguagem de programação, aqui o problema em questão é calcular
2+2
):O que há de errado com a primeira abordagem? Retorna 4 após um período de tempo razoavelmente curto; isso está correto. As objeções (juntamente com todos os motivos usuais que um desenvolvedor pode apresentar para refutá-las) são:
bash
(mas nunca será executado no Windows, Mac ou Android)fonte
fundamental contract of a constructor: to construct something
está no local.Você fornece razões suficientes em sua pergunta para descartar essa abordagem, mas a pergunta real aqui é "Existe algo mais obviamente ruim ou desonesto com essa abordagem?"
Meu primeiro pensamento aqui foi que isso não faz sentido. Se seu construtor nunca terminar, nenhuma outra parte do programa poderá obter uma referência ao objeto construído, então qual é a lógica por trás de colocá-lo em um construtor em vez de um método regular. Ocorreu-me que a única diferença seria que, no seu loop, você poderia permitir referências a
this
escape parcialmente construído do loop. Embora isso não seja estritamente limitado a essa situação, é garantido que, se você permitirthis
escapar, essas referências sempre apontarão para um objeto que não foi totalmente construído.Não sei se a semântica desse tipo de situação está bem definida em C #, mas eu diria que isso não importa, porque não é algo que a maioria dos desenvolvedores gostaria de tentar investigar.
fonte
this
no construtor - mas apenas se você estiver no construtor da classe mais derivada. Se você tiver duas classes,A
eB
comB : A
, se o construtor deA
vazarthis
, os membrosB
ainda serão não inicializados (e o método virtual chama ou lança paraB
causar estragos com base nisso).+1 Para menos espanto, mas esse é um conceito difícil de articular com novos desenvolvedores. Em um nível pragmático, é difícil depurar exceções geradas nos construtores, se o objeto falhar ao inicializar, não existirá para você inspecionar o estado ou o log de fora desse construtor.
Se você sentir a necessidade de fazer esse tipo de padrão de código, use métodos estáticos na classe.
Existe um construtor para fornecer a lógica de inicialização quando um objeto é instanciado a partir de uma definição de classe. Você constrói essa instância do objeto porque ela é um contêiner que encapsula um conjunto de propriedades e funcionalidades que deseja chamar do restante da lógica do aplicativo.
Se você não pretende usar o objeto que está "construindo", qual é o sentido de instanciar o objeto em primeiro lugar? Ter um loop while (true) em um construtor significa efetivamente que você nunca pretende que ele seja concluído ...
C # é uma linguagem orientada a objetos muito rica, com muitas construções e paradigmas diferentes para você explorar, conhecer suas ferramentas e quando usá-las.
fonte
Não há nada inerentemente ruim nisso. No entanto, o que será importante é a questão de por que você escolheu usar esse construto. O que você conseguiu fazendo isso?
Primeiro, não vou falar sobre o uso
while(true)
em um construtor. Não há absolutamente nada de errado em usar essa sintaxe em um construtor. No entanto, o conceito de "iniciar um ciclo de jogo no construtor" é aquele que pode causar alguns problemas.É muito comum nessas situações fazer com que o construtor simplesmente construa o objeto e tenha uma função que chama o loop infinito. Isso dá ao usuário do seu código mais flexibilidade. Como exemplo dos tipos de problemas que podem aparecer, você restringe o uso do seu objeto. Se alguém quiser ter um objeto membro que seja "o objeto do jogo" em sua classe, precisará estar pronto para executar todo o ciclo do jogo em seu construtor, pois precisará construir o objeto. Isso pode levar à codificação torturada para solucionar esse problema. No caso geral, você não pode pré-alocar seu objeto de jogo e executá-lo mais tarde. Para alguns programas, isso não é um problema, mas existem classes de programas nos quais você deseja alocar tudo antecipadamente e depois chamar o loop do jogo.
Uma das regras práticas da programação é usar a ferramenta mais simples para o trabalho. Não é uma regra difícil e rápida, mas é muito útil. Se uma chamada de função fosse suficiente, por que se preocupar em construir um objeto de classe? Se eu vir uma ferramenta complicada mais poderosa usada, presumo que o desenvolvedor tenha uma razão para isso e começarei a explorar que tipos de truques estranhos você pode estar fazendo.
Há casos em que isso pode ser razoável. Pode haver casos em que é realmente intuitivo você ter um loop de jogo no construtor. Nesses casos, você corre com ele! Por exemplo, o loop do jogo pode ser incorporado em um programa muito maior que cria classes para incorporar alguns dados. Se você precisar executar um loop de jogo para gerar esses dados, pode ser muito razoável executar o loop de jogo em um construtor. Apenas trate isso como um caso especial: seria válido fazer isso porque o programa abrangente torna intuitivo para o próximo desenvolvedor entender o que você fez e por quê.
fonte
GameTestResults testResults = runGameTests(tests, configuration);
ao invés deGameTestResults testResults = new GameTestResults(tests, configuration);
.Minha impressão é que alguns conceitos se confundiram e resultaram em uma pergunta não tão bem formulada.
O construtor de uma classe pode iniciar um novo thread que "nunca" terminará (embora eu prefira usar
while(_KeepRunning)
maiswhile(true)
porque posso definir o membro booleano como false em algum lugar).Você pode extrair esse método de criar e iniciar o encadeamento em uma função própria, a fim de separar a construção do objeto e o início real do seu trabalho - eu prefiro assim porque tenho melhor controle sobre o acesso aos recursos.
Você também pode dar uma olhada no "Padrão de Objeto Ativo". No final, acho que a pergunta foi direcionada para quando iniciar o encadeamento de um "Objeto Ativo", e minha preferência é o desacoplamento da construção e o início para um melhor controle.
fonte
Seu objeto me lembra de iniciar tarefas . O objeto de tarefa tem um construtor, mas também há um monte de métodos de fábrica estáticos como:
Task.Run
,Task.Start
eTask.Factory.StartNew
.Eles são muito parecidos com o que você está tentando fazer e é provável que seja a 'convenção'. A principal questão que as pessoas parecem ter é que esse uso de um construtor as surpreende. @ JörgWMittag diz que quebra um contrato fundamental , o que suponho significa que Jörg está muito surpreso. Eu concordo e não tenho mais nada a acrescentar.
No entanto, quero sugerir que o OP tente métodos de fábrica estáticos. A surpresa desaparece e não há contrato fundamental em jogo aqui. As pessoas estão acostumadas a métodos estáticos de fábrica fazendo coisas especiais e podem ser nomeadas de acordo.
Você pode fornecer construtores que permitam um controle refinado (como o @Brandin sugere, algo como
var g = new Game {...}; g.MainLoop();
, que acomoda usuários que não desejam iniciar o jogo imediatamente e talvez queiram repassá-lo primeiro. E você pode escrever algo comovar runningGame = Game.StartNew();
iniciar imediatamente fácil.fonte
Isso não é ruim.
Por que você iria querer fazer isso? A sério. Essa classe tem uma única função: o construtor. Se tudo o que você deseja é uma única função, é por isso que temos funções, não construtores.
Você mencionou algumas das razões óbvias contra isso, mas deixou de lado a maior:
Existem opções melhores e mais simples para conseguir a mesma coisa.
Se houver duas opções e uma for melhor, não importa o quanto seja melhor, você escolherá a melhor. Aliás, é por isso
quequase nunca usamos o GoTo.fonte
O objetivo de um construtor é, bem, contruir seu objeto. Antes que o construtor seja concluído, é, em princípio, inseguro usar qualquer método desse objeto. Obviamente, podemos chamar métodos do objeto dentro do construtor, mas, sempre que o fazemos, temos que garantir que isso seja válido para o objeto em seu estado atual de meia-baqueta.
Ao colocar indiretamente toda a lógica no construtor, você adiciona mais carga desse tipo a si mesmo. Isso sugere que você não projetou a diferença entre inicialização e uso suficiente.
fonte