Por que um loop while (true) em um construtor é realmente ruim?

47

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?

Samuel
fonte
61
Por que isso é bom? Se você simplesmente mover o loop principal para um método (refatoração muito simples), o usuário poderá escrever um código não surpreendente como este:var g = new Game {...}; g.MainLoop();
Brandin
61
Sua pergunta já indica seis razões para não usá-la, e eu gostaria de ver uma a seu favor. Você também pode perguntar por que é uma má idéia colocar um while(true)loop em um configurador de propriedades new Game().RunNow = true:?
Groo
38
Analogia extrema: por que dizemos que o que Hitler fez estava errado? Exceto pela discriminação racial, iniciando a Segunda Guerra Mundial, matando milhões de pessoas, violência [etc etc por mais de 50 outras razões], ele não fez nada de errado. Certamente, se você remover uma lista arbitrária de razões pelas quais algo está errado, você também poderá concluir que essas coisas são boas, literalmente para qualquer coisa.
Bakuriu 8/09/16
4
Com relação à coleta de lixo (GC) (seu problema técnico), não haveria nada de especial nisso. A instância existe antes que o código do construtor seja realmente executado. Nesse caso, a instância ainda está acessível e, portanto, não pode ser reivindicada pelo GC. Se o GC for configurado, essa instância sobreviverá e, a cada ocorrência de configuração do GC, o objeto será promovido para a próxima geração, de acordo com a regra usual. Se o GC ocorre ou não, depende se novos objetos (suficientes) estão sendo criados ou não.
Jeppe Stig Nielsen
8
Eu daria um passo adiante e diria que o tempo (verdadeiro) é ruim, independentemente de onde é usado. Isso implica que não há uma maneira clara e limpa de interromper o loop. No seu exemplo, o loop não deve parar quando o jogo é encerrado ou perde o foco?
Jon Anderson

Respostas:

250

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.

Jörg W Mittag
fonte
11
Talvez eles quisessem colocar um tempo (verdadeiro) com uma pausa no meio? Arriscado, mas legítimo.
John Dvorak
2
Eu concordo, isso está correto - pelo menos. do ponto de vista acadêmico. O mesmo vale para todas as funções que retornam algo.
Doc Brown
61
Imagine a pobre coitado que cria uma sub-classe da referida classe ...
Newtopian
5
@ JanDvorak: Bem, essa é uma pergunta diferente. O OP especificou explicitamente "sem fim" e mencionou um loop de eventos.
Jörg W Mittag
4
@ JörgWMittag bem, então, sim, não coloque isso em um construtor.
John Dvorak
45

Existe algo mais obviamente ruim ou desonesto com essa abordagem?

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):

$sum_a = `bash -c "echo $((2+2)) 2>/dev/null"`;   # calculate
chomp $sum_a;                         # remove trailing \n
$sum_a = $sum_a + 0;                  # force it to be a number in case some non-digit characters managed to sneak in

$sum_b = 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:

  • Mais difícil de ler (mas ei, um bom programador pode lê-lo tão facilmente, e sempre o fizemos assim; se você quiser, posso refatorá-lo para um método!)
  • Mais lento (mas este não está em um momento crítico para que possamos ignorar que, além disso, é melhor otimizar apenas quando necessário!)
  • Usa muito mais recursos (um novo processo, mais RAM etc. - mas aqui, nosso servidor é mais do que rápido o suficiente)
  • Introduz dependências bash(mas nunca será executado no Windows, Mac ou Android)
  • E todas as outras razões mencionadas acima.
AnoE
fonte
5
"O GC pode estar em um estado de bloqueio excepcional ao executar um construtor" - por quê? O alocador aloca primeiro e depois inicia o construtor como um método comum. Não há nada realmente especial sobre isso, existe? E o alocador precisa permitir novas alocações (e isso pode disparar situações de OOM) dentro do construtor de qualquer maneira.
John Dvorak
1
@ JanDvorak, Só queria enfatizar que poderia haver algum comportamento especial "em algum lugar" enquanto estiver em um construtor, mas você está correto, o exemplo que escolhi não era realista. Removido.
AnoE
Eu realmente gosto da sua explicação e gostaria de marcar as duas respostas como respostas (tenha pelo menos um voto positivo). A analogia é boa e sua trilha de pensamento e explicação dos custos secundários também é, mas eu os considero como requisitos auxiliares de código (o mesmo que meus argumentos). O estado da técnica atual é testar o código; portanto, a testabilidade etc. é um requisito de fato. Mas a declaração de @ JörgWMittag fundamental contract of a constructor: to construct somethingestá no local.
Samuel
A analogia não é tão boa. Se eu vir isso, espero ver um comentário em algum lugar sobre por que você está usando o Bash para fazer as contas e, dependendo do motivo, pode ser totalmente bom. O mesmo não funcionaria para o OP, porque você basicamente teria que se desculpar nos comentários em todos os lugares em que usava o construtor "// Nota: a construção desse objeto inicia um loop de eventos que nunca retorna".
Brandin
4
"Infelizmente, você regularmente encontra programadores que ignoram completamente tudo isso. Funciona, é isso." Tão triste, mas tão verdadeiro. Acho que posso falar por todos nós quando digo que o único fardo substancial em nossas vidas profissionais são, bem, essas pessoas.
Lightness Races com Monica
8

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 thisescape parcialmente construído do loop. Embora isso não seja estritamente limitado a essa situação, é garantido que, se você permitir thisescapar, 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.

JimmyJames
fonte
Eu diria que o construtor é executado depois que o objeto foi criado, portanto, em uma linguagem sã vazando, esse deve ser um comportamento perfeitamente bem definido.
mroman
@mroman: pode ser bom vazar thisno construtor - mas apenas se você estiver no construtor da classe mais derivada. Se você tiver duas classes, Ae Bcom B : A, se o construtor de Avazar this, os membros Bainda serão não inicializados (e o método virtual chama ou lança para Bcausar estragos com base nisso).
hoffmale
1
Eu acho que em C ++ isso pode ser louco, sim. Em outros idiomas, os membros recebem um valor padrão antes de receberem seus valores reais, portanto, embora seja estúpido escrever esse código, o comportamento ainda está bem definido. Se você chamar métodos que usam esses membros, poderá executar NullPointerExceptions ou cálculos incorretos (porque os números são 0) ou coisas assim, mas é tecnicamente determinístico o que acontece.
mroman 11/09/16
@mroman Você pode encontrar uma referência definitiva para o CLR que mostre que esse é o caso?
JimmyJames
Bem, você pode atribuir valores aos membros antes que o construtor seja executado, para que o objeto exista em um estado válido com os membros atribuídos aos valores padrão ou aos valores atribuídos a eles antes mesmo da execução do construtor. Você não precisa do construtor para inicializar membros. Deixe-me ver se consigo encontrar isso em algum lugar nos documentos.
mroman 12/09/16
1

+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.

Em C # não execute lógica estendida ou interminável dentro de construtores porque ... existem alternativas melhores

Chris Schaller
fonte
1
sua não parece oferecer nada substancial sobre pontos feitos e explicado em anteriores 9 respostas
mosquito
1
Ei, eu tive que tentar :) Pensei que era importante destacar que, embora não exista regra contra isso, você não deve, em virtude do fato de que existem maneiras mais simples. A maioria das outras respostas focar o tecnicismo de por que é ruim, mas que é difícil de vender para um novo dev que diz "mas ele compila, então eu só posso deixá-lo assim"
Chris Schaller
0

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ê.

Cort Ammon
fonte
Se a construção do seu objeto requer um loop de jogo, pelo menos coloque-o em um método de fábrica. GameTestResults testResults = runGameTests(tests, configuration);ao invés de GameTestResults testResults = new GameTestResults(tests, configuration);.
User253751
@immibis Sim, isso é verdade, exceto nos casos em que isso não é razoável. Se você estiver trabalhando com um sistema baseado em reflexão que instancia seu objeto para você, talvez não tenha a opção de criar um método de fábrica.
Cort Ammon
0

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)mais while(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.

Bernhard Hiller
fonte
0

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.Starte Task.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 como var runningGame = Game.StartNew();iniciar imediatamente fácil.

Nathan Cooper
fonte
Isso significa que Jörg é surpreendido fundamentalmente , ou seja, uma surpresa como essa é uma dor no fundamento.
9788 Steve Jobs
0

Por que um loop while (true) em um construtor é realmente ruim?

Isso não é ruim.

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)?

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 que quase nunca usamos o GoTo.

Peter
fonte
-1

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.

Hagen von Eitzen
fonte
1
este não parece oferecer nada substancial sobre pontos feitos e explicado em anteriores 8 respostas
mosquito