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

47

Embora seja uma questão geral, meu escopo é mais C #, pois estou ciente de que linguagens como C ++ possuem 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 não foi facilmente respondida.

Por que (ou é mesmo?) considerado como um design ruim permite que um construtor de uma classe inicie um loop sem fim (isto é, loop de jogo)?

Existem alguns conceitos que são quebrados por isso:

  • como o princípio de menos espanto, o usuário não espera que o construtor se comporte assim.
  • Os testes de unidade são mais difíceis, pois você não pode criar essa classe ou injetá-la, pois ela nunca sai do loop.
  • O final do loop (final do jogo) é conceitualmente o tempo em que o construtor termina, o que também é estranho.
  • Tecnicamente, essa classe não possui membros públicos, exceto o construtor, o que dificulta o entendimento (especialmente para idiomas em que nenhuma implementação está disponível)

E depois há questões técnicas:

  • O construtor nunca termina, então o que acontece com o GC aqui? Este objeto já está no Gen 0?
  • Derivar de tal classe é impossível ou pelo menos muito complicado devido ao fato de que o construtor de base nunca retorna

Existe algo mais obviamente ruim ou desonesto com tal abordagem?

    
por Samuel 08.09.2016 / 11:04
fonte

9 respostas

249

Qual é o propósito de um construtor? Ele retorna um objeto recém-construído. O que faz um loop infinito? Isso nunca retorna. Como o construtor pode retornar um objeto recém-construído se ele não retornar? Não pode.

Ergo, um loop infinito quebra o contrato fundamental de um construtor: construir algo.

    
por 08.09.2016 / 12:59
fonte
44

Is there something more obviously bad or devious with such an approach?

Sim, claro. É desnecessário, inesperado, inútil, deselegante. Viola os conceitos modernos de design de classes (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 sustentável, futuros programadores vão gastar muito tempo tentando entender o que está acontecendo, e tentando adivinhar as razões pelas quais isso foi feito dessa maneira.

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, difícil de adicionar testes, difícil de reutilizar, difícil de depurar, difícil de estender etc. .).

Muitos / os aprimoramentos mais modernos nos métodos de desenvolvimento de software são feitos especificamente para facilitar o processo real de escrever / testar / depurar / manter o software. Tudo isso é contornado por coisas como essa, em que o código é colocado aleatoriamente porque "funciona".

Infelizmente, você encontrará regularmente programadores que são completamente ignorantes de tudo isso. 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 depois de um tempo razoavelmente curto; isso está correto. As objeções (juntamente com todas as razões usuais que um desenvolvedor pode dar para refutá-las) são:

  • Mais difícil de ler (mas, ei, um bom programador pode lê-lo com facilidade, e sempre fizemos assim: se você quiser, posso refatorá-lo em um método!)
  • Mais lento (mas isso não está em um momento crítico de tempo, então podemos ignorar isso e, além disso, é melhor otimizá-lo somente quando necessário!)
  • Usa muito mais recursos (um novo processo, mais memória RAM etc. - mas dito, nosso servidor é mais do que rápido o suficiente)
  • Introduz dependências em bash (mas nunca será executado no Windows, Mac ou Android)
  • E todas as outras razões mencionadas acima.
por 08.09.2016 / 18:12
fonte
8

Você dá razões suficientes em sua pergunta para descartar essa abordagem, mas a questão real aqui é "Existe algo mais obviamente ruim ou desonesto com tal abordagem?"

Meu primeiro aqui foi que isso é inútil. Se seu construtor nunca termina, nenhuma outra parte do programa pode obter uma referência ao objeto construído , então qual é a lógica por trás de colocá-lo em um construtor ao invés de um método regular. Então ocorreu-me que a única diferença seria que, no seu loop, você poderia permitir referências a parcialmente construído this do loop. Embora isso não seja estritamente limitado a essa situação, é garantido que, se você permitir que this escape, essas referências sempre apontarão para um objeto que não esteja totalmente construído.

Eu não sei se a semântica em torno deste tipo de situação é bem definida em C #, mas eu diria que não importa, porque não é algo que a maioria dos desenvolvedores gostaria de tentar aprofundar.

    
por 08.09.2016 / 18:58
fonte
1

+1 Pelo menos espanto, mas este é um conceito difícil de articular para novos desenvolvedores. Em um nível pragmático, é difícil depurar exceções levantadas dentro de construtores, se o objeto falhar ao inicializar, não existirá para você inspecionar o estado ou log de fora daquele construtor.

Se você sentir 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 de objeto porque é um contêiner que encapsula um conjunto de propriedades e funcionalidades que você deseja chamar do restante da sua lógica de aplicativo.

Se você não pretende usar o objeto que está "construindo", qual é o objetivo de instanciar o objeto? Ter um loop while (true) em um construtor significa que você nunca pretende que ele seja concluído ...

C # é uma linguagem orientada a objetos muito rica, com muitos construtos e paradigmas diferentes para você explorar, conhecer suas ferramentas e quando usá-las, resumindo:

In C# do not execute extended or never-ending logic within constructors because... there are better alternatives

    
por 10.09.2016 / 16:37
fonte
0

Não há nada intrinsecamente ruim nisso. No entanto, o que será importante é a questão de por que você escolheu usar esse constructo. O que você conseguiu fazendo isso?

Primeiro, não vou falar sobre o uso de 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 loop de jogo no construtor" é aquele que pode causar alguns problemas.

É muito comum nessas situações que o construtor simplesmente construa o objeto e tenha uma função que chame o loop infinito. Isso dá ao usuário do seu código mais flexibilidade. Como um exemplo dos tipos de problemas que podem aparecer é que você restringe o uso do seu objeto. Se alguém quiser ter um objeto membro que seja "o objeto do jogo" em sua classe, ele deve estar pronto para executar todo o loop do jogo em seu construtor, porque eles precisam construir o objeto. Isso pode levar a uma codificação torturada para contornar isso. No caso geral, você não pode pré-alocar seu objeto de jogo e depois executá-lo mais tarde. Para alguns programas, isso não é um problema, mas há classes de programas em que você deseja alocar tudo na frente e depois chamar o loop do jogo.

Uma das regras básicas 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 vejo uma ferramenta complicada mais poderosa usada, presumo que o desenvolvedor tenha uma razão para isso, e vou começar a explorar que tipos de truques estranhos você pode estar fazendo.

Existem casos em que isso pode ser razoável. Pode haver casos em que é realmente intuitivo para você ter um loop de jogo no construtor. Nesses casos, você corre com isso! Por exemplo, seu loop de jogos pode ser incorporado em um programa muito maior que constrói classes para incorporar alguns dados. Se você tiver que executar um loop de jogo para gerar esses dados, pode ser muito razoável executar o loop do 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ê.

    
por 08.09.2016 / 21:21
fonte
0

Minha impressão é que alguns conceitos se confundiram e resultaram em uma pergunta não muito bem formulada.

O construtor de uma classe pode iniciar um novo encadeamento que "nunca" terminará (embora eu prefira usar while(_KeepRunning) over while(true) porque eu posso definir o membro booleano como false em algum lugar).

Você poderia 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 de seu trabalho - eu prefiro assim porque obtenho melhor controle sobre o acesso aos recursos. / p>

Você também pode dar uma olhada no "Padrão de Objeto Ativo". No fim das contas, acho que a questão era quando iniciar o thread de um "Objeto Ativo", e minha preferência é um desacoplamento de construção e início para melhor controle.

    
por 09.09.2016 / 09:07
fonte
0

Seu objeto me lembra de iniciar as Tarefas . O objeto de tarefa tem um construtor, mas também há vários métodos de fábrica estáticos, como: Task.Run , Task.Start e Task.Factory.StartNew .

Estes são muito semelhantes ao que você está tentando fazer, e é provável que esta 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 surpreendido. Eu concordo e não tenho mais nada para acrescentar a isso.

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 de fábrica estáticos fazendo coisas especiais e podem ser nomeadas de acordo.

Você pode fornecer construtores que permitem um controle refinado (como sugere o @Brandin, algo como var g = new Game {...}; g.MainLoop(); , que acomoda os usuários que não querem iniciar o jogo imediatamente e talvez queira passá-lo primeiro. E você pode escreva algo como var runningGame = Game.StartNew(); para facilitar imediatamente.

    
por 09.09.2016 / 14:54
fonte
0

Why is a while(true) loop in a constructor actually bad?

Isso não é nada mal.

Why (or is it at all?) regarded as bad design to let a constructor of a class start a never ending loop (i.e. game loop)?

Por que você iria querer fazer isso? A sério. Essa classe tem uma única função: o construtor. Se tudo o que você quer é uma única função, é por isso que temos funções, não construtores.

Você mencionou algumas das razões óbvias contra você mesmo, mas deixou de fora a maior delas:

Existem opções melhores e mais simples para alcançar a mesma coisa.

Se houver 2 opções e uma é melhor, não importa o quanto é melhor, você escolheu a melhor. Aliás, é por isso que não quase nunca usa o GoTo.

    
por 12.09.2016 / 13:05
fonte
-1

O propósito de um construtor é, bem, contruir seu objeto. Antes de o construtor ser concluído, não é, em princípio, seguro utilizar quaisquer métodos desse objeto. É claro que podemos chamar métodos do objeto dentro do construtor, mas sempre que fizermos isso, temos que ter certeza de que fazer isso é válido para o objeto em seu estado atual meio-baken.

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 bem o suficiente.

    
por 10.09.2016 / 11:22
fonte