As exceções como fluxo de controle são consideradas um antipadrão sério? Se sim, por quê?

129

No final dos anos 90, trabalhei bastante com uma base de código que usava exceções como controle de fluxo. Ele implementou uma máquina de estado finito para direcionar aplicativos de telefonia. Ultimamente, sou lembrado desses dias porque tenho feito aplicativos da Web MVC.

Ambos têm Controllers que decidem para onde ir em seguida e fornecem os dados para a lógica de destino. As ações do usuário do domínio de um telefone antigo, como tons DTMF, tornaram-se parâmetros para os métodos de ação, mas, em vez de retornar algo como a ViewResult, eles deram a StateTransitionException.

Eu acho que a principal diferença foi que os métodos de ação eram voidfunções. Não me lembro de todas as coisas que fiz com esse fato, mas hesitei em lembrar de muita coisa porque, desde aquele trabalho, como há 15 anos, nunca vi isso no código de produção em nenhum outro trabalho . Eu assumi que isso era um sinal de que era um chamado anti-padrão.

É esse o caso? Em caso afirmativo, por quê?

Atualização: quando fiz a pergunta, eu já tinha em mente a resposta do @ MasonWheeler, então fui com a resposta que mais me agradava. Eu acho que a dele também é uma boa resposta.

Anodeto de Aaron
fonte
2
Questão relacionada: programmers.stackexchange.com/questions/107723
Eric Rei
25
Não. No Python, usar exceções como fluxo de controle é considerado "pitônico".
user16764
4
Se eu fizesse algo como Java, certamente não lançaria uma exceção a ele. Eu derivaria de uma hierarquia não-exceção, não-erro e jogável.
Thomas Eding
2
Para adicionar às respostas existentes, aqui está uma breve orientação que me serviu bem: - Nunca use exceções para "o caminho feliz". O caminho feliz pode ser (para a web) toda a solicitação ou simplesmente um objeto / método. Todas as outras regras sãs ainda se aplicam, é claro :) #
Houen 18/03/16
8
As exceções não estão sempre controlando o fluxo de um aplicativo?

Respostas:

109

Há uma discussão detalhada sobre isso no Wiki de Ward . Geralmente, o uso de exceções para controle de fluxo é um anti-padrão, com muitos situação notável - e de idioma específico (ver, por exemplo Python ) tosse exceções tosse .

Como um resumo rápido do motivo, geralmente, é um antipadrão:

  • Exceções são, em essência, sofisticadas declarações GOTO
  • Programar com exceções, portanto, leva a mais difícil leitura e compreensão do código
  • A maioria dos idiomas possui estruturas de controle existentes projetadas para resolver seus problemas sem o uso de exceções
  • Os argumentos de eficiência tendem a ser discutíveis para os compiladores modernos, que tendem a otimizar com a suposição de que as exceções não são usadas para o fluxo de controle.

Leia a discussão no wiki de Ward para obter informações muito mais detalhadas.


Veja também uma duplicata desta pergunta, aqui

blueberryfields
fonte
18
Sua resposta faz parecer que exceções são ruins para todas as circunstâncias, enquanto a pergunta é focada em exceções como controle de fluxo.
Whatsisname
14
@MasonWheeler A diferença é que os loops for / while contêm claramente suas alterações no controle de fluxo e facilitam a leitura do código. Se você vir uma instrução for no seu código, não precisará tentar descobrir qual arquivo contém o final do loop. Goto não é ruim porque algum deus disse que era, é ruim simplesmente porque é mais difícil de seguir do que as construções em loop. As exceções são semelhantes, não impossíveis, mas difíceis o suficiente para confundir as coisas.
Bill K
24
@ BillK, então argumente isso e não faça declarações simplistas sobre como as exceções são gotos.
Winston Ewert
6
Ok, mas sério, o que há com os desenvolvedores do aplicativo e do servidor que enterram erros com instruções de captura vazias no JavaScript? É um fenômeno desagradável que me custou muito tempo e não sei perguntar sem reclamar. Os erros são seus amigos.
Erik Reppen
12
@mattnz: ife foreachtambém são sofisticados GOTO. Francamente, acho que a comparação com goto não é útil; é quase como um termo depreciativo. O uso do GOTO não é intrinsecamente mau - ele tem problemas reais e as exceções podem compartilhá-los, mas não podem. Seria mais útil ouvir esses problemas.
Eamon Nerbonne
110

O caso de uso para o qual as exceções foram criadas é "Acabei de encontrar uma situação com a qual não posso lidar adequadamente neste momento, porque não tenho contexto suficiente para lidar com isso, mas a rotina que me chamou (ou algo mais adiante na chamada pilha) deve saber como lidar com isso ".

O caso de uso secundário é "Acabei de encontrar um erro grave e, neste momento, sair desse fluxo de controle para evitar corrupção de dados ou outros danos é mais importante do que tentar continuar adiante".

Se você não estiver usando exceções por um desses dois motivos, provavelmente há uma maneira melhor de fazer isso.

Mason Wheeler
fonte
18
Isso não responde à pergunta, no entanto. O que eles foram projetados é irrelevante; a única coisa que é relevante é por que usá-los para o fluxo de controle é ruim, um tópico que você não tocou. Como exemplo, os modelos C ++ foram projetados para uma coisa, mas são perfeitamente adequados para serem usados ​​na metaprogramação, um uso que os designers nunca anteciparam.
Thomas Bonini
6
@Krelp: Os designers nunca anteciparam muitas coisas, como acabar com um sistema de modelos completo de Turing por acidente! Modelos C ++ dificilmente são um bom exemplo para usar aqui.
Mason Wheeler
15
@ Krelp - modelos C ++ NÃO são 'perfeitamente adequados' para metaprogramação. Eles são um pesadelo até você acertá-los e tendem a usar código somente para gravação se você não for um gênio do modelo. Você pode escolher um exemplo melhor.
Michael Kohne
Exceções prejudicam a composição da função em particular e a brevidade do código em geral. Por exemplo, enquanto eu era inexperiente, usei uma exceção em um projeto, e isso causou problemas por muito tempo, porque 1) as pessoas precisam se lembrar de pegá-lo, 2) você não pode escrever const myvar = theFunctionporque o myvar precisa ser criado a partir do try-catch, assim, não é mais constante. Isso não significa que eu não o uso em C #, pois é um mainstream lá por qualquer motivo, mas estou tentando reduzi-los de qualquer maneira.
Hi-Angel
2
@AndreasBonini O que eles são projetados / destinados a questões, porque geralmente resulta em decisões de implementação de compilador / estrutura / tempo de execução alinhadas com o design. Por exemplo, lançar uma exceção pode ser muito mais “caro” do que um simples retorno porque ele recolhe informação destina-se a ajudar alguém a depuração do código, como rastreamentos de pilha, etc
binki
29

Exceções são tão poderosas quanto Continuações e GOTO. Eles são uma construção de fluxo de controle universal.

Em alguns idiomas, eles são o único construto de fluxo de controle universal. O JavaScript, por exemplo, não tem Continuações nem GOTOpossui chamadas de cauda apropriadas. Então, se você quiser implementar o fluxo de controle sofisticados em JavaScript, você tem de usar exceções.

O projeto Microsoft Volta era um projeto de pesquisa (agora descontinuado) para compilar código .NET arbitrário em JavaScript. O .NET possui exceções cuja semântica não é exatamente mapeada para JavaScript, mas, mais importante, possui Threads , e você precisa mapeá-las de alguma forma para JavaScript. Volta fez isso implementando Volta Continuations usando exceções JavaScript e, em seguida, implemente todas as construções de fluxo de controle .NET em termos de Volta Continuations. Eles tiveram que usar Exceções como fluxo de controle, porque não há outra construção de fluxo de controle suficientemente poderosa.

Você mencionou State Machines. As SMs são triviais para implementar com chamadas de cauda apropriadas: todo estado é uma sub-rotina, toda transição de estado é uma chamada de sub-rotina. As SMs também podem ser facilmente implementadas com GOTOCorotinas ou Continuações. No entanto, Java não tem qualquer um desses quatro, mas não tem exceções. Portanto, é perfeitamente aceitável usá-los como fluxo de controle. (Bem, na verdade, a escolha correta provavelmente seria usar uma linguagem com a construção de fluxo de controle apropriada, mas às vezes você pode ficar com o Java.)

Jörg W Mittag
fonte
7
Se tivéssemos tido a recursão adequada da chamada de cauda, ​​talvez pudéssemos dominar o desenvolvimento da Web do lado do cliente com JavaScript e, em seguida, seguimos expandindo para o lado do servidor e para o celular como a melhor solução de plataforma em plataforma como o incêndio. Infelizmente, mas não foi assim. Condene nossas insuficientes funções de primeira classe, fechamentos e paradigma orientado a eventos. O que realmente precisamos era real.
precisa
20
@ErikReppen: Sei que você está sendo sarcástico, mas. . . Eu realmente não acho que o fato de que "têm dominado desenvolvimento web do lado do cliente com JavaScript" tem nada a ver com as características da linguagem. Ele tem o monopólio nesse mercado e, portanto, conseguiu se livrar de muitos problemas que não podem ser eliminados com o sarcasmo.
Ruakh
11
A recursão da cauda teria sido um bônus interessante (eles estão depreciando os recursos das funções para tê-lo no futuro), mas sim, eu diria que venceu contra VB, Flash, Applets, etc ... por motivos relacionados a recursos. Se não reduzisse a complexidade e normalizasse com a mesma facilidade, teria tido concorrência real em algum momento. Atualmente, estou executando o Node.js para lidar com a reescrita de 21 arquivos de configuração para uma pilha horrível de C # + Java e sei que não sou o único por aí a fazer isso. É muito bom no que é bom.
precisa
11
Muitas linguagens não possuem gotos (considerados perigosos!), Corotinas ou continuações e implementam um controle de fluxo perfeitamente válido simplesmente usando ifs, whiles e chamadas de função (ou gosubs!). De fato, a maioria delas pode ser usada para emular-se, assim como uma continuação pode ser usada para representar todos os itens acima. (Por exemplo, você pode usar um tempo para executar um if, mesmo que seja uma maneira fedida de codificar). Portanto, NÃO são necessárias exceções para executar o controle avançado de fluxo.
Shayne
2
Bem, sim, você pode estar certo sobre se. "For" em sua forma de estilo C, no entanto, pode ser usado como um tempo desajeitado, e um tempo pode ser usado juntamente com um if para implementar uma máquina de estado finito que pode emular todas as outras formas de controle de fluxo. Novamente, maneira fedida para codificar, mas sim. E, novamente, foi considerado prejudicial. (E eu argumentaria, também com o uso de exceções em circunstâncias não excepcionais). Lembre-se de que existem linguagens perfeitamente válidas e muito poderosas que não fornecem goto nem exceções e funcionam muito bem.
precisa
19

Como outros já mencionaram numerosamente ( por exemplo, nesta questão do Stack Overflow ), o princípio de menor espanto proibirá o uso excessivo de exceções apenas para fins de controle de fluxo. Por outro lado, nenhuma regra é 100% correta e sempre há casos em que uma exceção é "a ferramenta certa" - a exemplo de gotosi mesma, a propósito, que é fornecida na forma breake continueem linguagens como Java, que geralmente são a maneira perfeita de saltar de loops muito aninhados, que nem sempre são evitáveis.

A seguinte postagem no blog explica um caso de uso bastante complexo, mas também bastante interessante, para um não local ControlFlowException :

Ele explica como dentro do jOOQ (uma biblioteca de abstração SQL para Java) (isenção de responsabilidade: eu trabalho para o fornecedor), essas exceções são ocasionalmente usadas para abortar o processo de renderização SQL mais cedo, quando alguma condição "rara" é atendida.

Exemplos de tais condições são:

  • Muitos valores de ligação são encontrados. Alguns bancos de dados não oferecem suporte a números arbitrários de valores de ligação em suas instruções SQL (SQLite: 999, Ingres 10.1.0: 1024, Sybase ASE 15.5: 2000, SQL Server 2008: 2100). Nesses casos, o jOOQ interrompe a fase de renderização do SQL e renderiza novamente a instrução SQL com valores de ligação embutidos. Exemplo:

    // Pseudo-code attaching a "handler" that will
    // abort query rendering once the maximum number
    // of bind values was exceeded:
    context.attachBindValueCounter();
    String sql;
    try {
    
      // In most cases, this will succeed:
      sql = query.render();
    }
    catch (ReRenderWithInlinedVariables e) {
      sql = query.renderWithInlinedBindValues();
    }
    

    Se extraíssemos explicitamente os valores de ligação da AST da consulta para contá-los todas as vezes, desperdiçamos ciclos valiosos de CPU para 99,9% das consultas que não sofrem com esse problema.

  • Alguma lógica está disponível apenas indiretamente por meio de uma API que queremos executar apenas "parcialmente". O UpdatableRecord.store()método gera uma instrução INSERTou UPDATE, dependendo Recorddos sinalizadores internos do. Do lado de fora, não sabemos em que tipo de lógica está contida store()(por exemplo, bloqueio otimista, manipulação de ouvinte de eventos etc.), portanto, não queremos repetir essa lógica quando armazenamos vários registros em uma instrução em lote, onde gostaríamos de store()gerar apenas a instrução SQL e não executá-la. Exemplo:

    // Pseudo-code attaching a "handler" that will
    // prevent query execution and throw exceptions
    // instead:
    context.attachQueryCollector();
    
    // Collect the SQL for every store operation
    for (int i = 0; i < records.length; i++) {
      try {
        records[i].store();
      }
    
      // The attached handler will result in this
      // exception being thrown rather than actually
      // storing records to the database
      catch (QueryCollectorException e) {
    
        // The exception is thrown after the rendered
        // SQL statement is available
        queries.add(e.query());                
      }
    }
    

    Se tivéssemos externalizado a store()lógica na API "reutilizável" que pode ser personalizada para opcionalmente não executar o SQL, estaríamos tentando criar uma API bastante difícil de manter e dificilmente reutilizável.

Conclusão

Em essência, nosso uso desses gotos não locais é exatamente como o que Mason Wheeler disse em sua resposta:

"Acabei de encontrar uma situação com a qual não posso lidar adequadamente neste momento, porque não tenho contexto suficiente para lidar com isso, mas a rotina que me chamou (ou algo mais acima na pilha de chamadas) deve saber como lidar com isso. . "

Ambos os usos ControlFlowExceptionsforam bastante fáceis de implementar em comparação com suas alternativas, permitindo reutilizar uma ampla gama de lógica sem refatorá-la dos internos relevantes.

Mas a sensação de que isso é uma surpresa para os futuros mantenedores permanece. O código parece bastante delicado e, embora tenha sido a escolha certa nesse caso, sempre preferimos não usar exceções para o fluxo de controle local , onde é fácil evitar o uso de ramificações comuns if - else.

Lukas Eder
fonte
11

O uso de exceções para o fluxo de controle é geralmente considerado um antipadrão, mas há exceções (sem trocadilhos).

Já foi dito mil vezes que exceções são destinadas a condições excepcionais. Uma conexão de banco de dados interrompida é uma condição excepcional. Um usuário digitando letras em um campo de entrada que deve permitir apenas números não é .

Um bug no seu software que faz com que uma função seja chamada com argumentos ilegais, por exemplo, nullonde não é permitido, é uma condição excepcional.

Usando exceções para algo que não é excepcional, você está usando abstrações inadequadas para o problema que está tentando resolver.

Mas também pode haver uma penalidade de desempenho. Alguns idiomas têm uma implementação de manipulação de exceção mais ou menos eficiente; portanto, se o seu idioma de escolha não tiver um tratamento de exceção eficiente, pode ser muito caro, em termos de desempenho *.

Mas outros idiomas, por exemplo, Ruby, têm uma sintaxe semelhante a exceção para o fluxo de controle. Situações excepcionais são tratadas pelos operadores raise/ rescue. Mas você pode usar throw/ catchpara construções de fluxo de controle de exceção **.

Portanto, embora as exceções geralmente não sejam usadas para o fluxo de controle, seu idioma de escolha pode ter outros idiomas.

* Por exemplo, o uso caro de exceções de desempenho: uma vez fui definido para otimizar um aplicativo ASP.NET Web Form com desempenho insatisfatório. Aconteceu que a renderização de uma mesa grande estava exigindo int.Parse()aprox. mil cadeias de caracteres vazias em uma página média, resultando em aprox. mil exceções sendo tratadas. Ao substituir o código por int.TryParse()eu raspei um segundo! Para cada solicitação de página única!

** Este pode ser muito confuso para um programador que vem para Ruby de outras línguas, pois ambos throwe catchsão palavras-chave associadas com exceções em muitas outras línguas.

Pete
fonte
11
+1 em "Ao usar exceções para algo que não é excepcional, você está usando abstrações inadequadas para o problema que está tentando resolver".
Giorgio
8

É completamente possível lidar com condições de erro sem o uso de exceções. Alguns idiomas, principalmente o C, nem sequer têm exceções, e as pessoas ainda conseguem criar aplicativos bastante complexos com ele. A razão pela qual as exceções são úteis é que elas permitem especificar sucintamente dois fluxos de controle essencialmente independentes no mesmo código: um se ocorrer um erro e outro se não ocorrer. Sem eles, você acaba com o código em todo o lugar que se parece com isso:

status = getValue(&inout);
if (status < 0)
{
    logError("message");
    return status;
}

doSomething(*inout);

Ou equivalente no seu idioma, como retornar uma tupla com um valor como status de erro, etc. Muitas vezes as pessoas que apontam o quão caro é o tratamento de exceções, negligenciam todas as ifinstruções extras como acima que você precisará adicionar se não Não use exceções.

Embora esse padrão ocorra com mais frequência ao lidar com erros ou outras "condições excepcionais", na minha opinião, se você começar a ver código padrão como esse em outras circunstâncias, terá um bom argumento para usar exceções. Dependendo da situação e implementação, posso ver as exceções sendo usadas validamente em uma máquina de estados, porque você tem dois fluxos de controle ortogonais: um que está alterando o estado e outro para os eventos que ocorrem dentro dos estados.

No entanto, essas situações são raras e, se você fizer uma exceção (trocadilhos) à regra, é melhor estar preparado para mostrar sua superioridade em relação a outras soluções. Um desvio sem essa justificativa é justamente chamado de antipadrão.

Karl Bielefeldt
fonte
2
Desde que essas declarações if falhem apenas em circunstâncias "excepcionais", a lógica de previsão de ramificação das CPUs modernas torna seu custo insignificante. Este é um lugar onde as macros podem realmente ajudar, desde que você seja cuidadoso e não tente fazer muito com elas.
James
5

No Python, as exceções são usadas para o encerramento do gerador e da iteração. O Python possui blocos try / except muito eficientes, mas, na verdade, gerar uma exceção tem alguma sobrecarga.

Devido à falta de quebras de vários níveis ou uma declaração goto no Python, às vezes eu usei exceções:

class GOTO(Exception):
  pass

try:
  # Do lots of stuff
  # in here with multiple exit points
  # each exit point does a "raise GOTO()"
except GOTO:
  pass
except Exception as e:
  #display error
gahooa
fonte
6
Se você precisar finalizar o cálculo em algum lugar de uma instrução profundamente aninhada e acessar algum código de continuação comum, esse caminho específico de execução provavelmente poderá ser fatorado como uma função, returnem vez de goto.
9000
3
@ 9000 Eu estava prestes a comentar exatamente a mesma coisa ... Mantenha os try:blocos em 1 ou 2 linhas, por favor, nunca try: # Do lots of stuff.
Wim
11
@ 9000, em alguns casos, com certeza. Mas então você perde o acesso a variáveis ​​locais e move seu código para outro lugar, quando o processo é um processo linear e coerente.
gahooa
4
@gahooa: Eu costumava pensar como você. Era um sinal de má estrutura do meu código. Quando pensei mais nisso, notei que os contextos locais podem ser desembaraçados e toda a bagunça transformada em funções curtas com poucos parâmetros, poucas linhas de código e significado muito preciso. Eu nunca olhei para trás.
9000
4

Vamos esboçar esse uso de exceção:

O algoritmo pesquisa recursivamente até que algo seja encontrado. Então, voltando da recursão, é preciso verificar o resultado para ser encontrado e retornar, caso contrário, continue. E isso repetidamente voltando de alguma profundidade de recursão.

Além de precisar de um booleano extra found(para ser empacotado em uma classe, onde, caso contrário, talvez apenas um int tivesse sido retornado), e para a profundidade da recursão, o mesmo postlude acontece.

Esse desenrolar de uma pilha de chamadas é exatamente o que é uma exceção. Portanto, parece-me um meio de codificação não-goto, mais imediato e apropriado. Não é necessário, uso raro, talvez com estilo ruim, mas direto ao ponto. Comparável com a operação de corte Prolog.

Joop Eggen
fonte
Hmm, é o voto negativo para a posição realmente contrária, ou para argumentação insuficiente ou curta? Eu sou realmente curioso.
Joop Eggen
Estou enfrentando exatamente essa situação, esperava que você pudesse ver o que acha da minha pergunta seguinte. Recursivamente usando exceções para acumular a razão (mensagem) para um alto nível de exceção codereview.stackexchange.com/questions/107862/...
CL22
@ Jodes Acabei de ler sua técnica interessante, mas estou bastante ocupada por enquanto. Espero que outra pessoa possa esclarecer sua questão.
Joop Eggen
4

Programação é sobre trabalho

Eu acho que a maneira mais fácil de responder a isso é entender o progresso que a OOP fez ao longo dos anos. Tudo o que é feito no OOP (e na maioria dos paradigmas de programação) é modelado em torno da necessidade de trabalho .

Toda vez que um método é chamado, o chamador está dizendo "Eu não sei como fazer esse trabalho, mas você sabe como, então você faz isso por mim".

Isso apresentou uma dificuldade: o que acontece quando o método chamado geralmente sabe como fazer o trabalho, mas nem sempre? Precisávamos de uma maneira de nos comunicar "Eu realmente queria ajudá-lo, mas realmente não posso fazer isso".

Uma metodologia inicial para comunicar isso era simplesmente retornar um valor "lixo". Talvez você espere um número inteiro positivo, portanto o método chamado retorna um número negativo. Outra maneira de conseguir isso era definir um valor de erro em algum lugar. Infelizmente, as duas formas resultaram no código padrão, deixe-me verificar aqui para garantir que tudo é kosher . À medida que as coisas ficam mais complicadas, esse sistema desmorona.

Uma analogia excepcional

Digamos que você tenha carpinteiro, encanador e eletricista. Você quer encanador para consertar sua pia, então ele dá uma olhada nela. Não é muito útil se ele disser apenas a você: "Desculpe, não posso consertar. Está quebrado." Inferno, é ainda pior se ele der uma olhada, sair e enviar uma carta para você dizendo que não poderia consertar. Agora você precisa verificar seu e-mail antes mesmo de saber que ele não fez o que você queria.

O que você prefere é que ele lhe diga: "Olha, eu não consegui consertar, porque parece que sua bomba não está funcionando".

Com essas informações, você pode concluir que deseja que o eletricista dê uma olhada no problema. Talvez o eletricista encontre algo relacionado ao carpinteiro, e você precisará que ele conserte.

Caramba, você pode nem saber que precisa de um eletricista, pode não saber de quem precisa. Você é apenas gerente de nível médio em um negócio de consertos domésticos e seu foco é o encanamento. Então você diz a seu chefe sobre o problema e ele diz ao eletricista para consertá-lo.

É isso que as exceções estão modelando: modos de falha complexos de maneira dissociada. O encanador não precisa saber sobre eletricista - ele nem precisa saber que alguém na cadeia pode resolver o problema. Ele apenas relata o problema que encontrou.

Então ... um anti-padrão?

Ok, entender o ponto de exceção é o primeiro passo. O próximo é entender o que é um antipadrão.

Para se qualificar como um antipadrão, ele precisa

  • resolva o problema
  • tem consequências definitivamente negativas

O primeiro ponto é facilmente encontrado - o sistema funcionou, certo?

O segundo ponto é mais pegajoso. O principal motivo para usar exceções como fluxo de controle normal é ruim é porque esse não é o objetivo delas. Qualquer funcionalidade fornecida em um programa deve ter um objetivo relativamente claro, e a cooptação desse objetivo gera confusão desnecessária.

Mas isso não é um dano definitivo . É uma maneira ruim de fazer as coisas, e estranha, mas um anti-padrão? Não. Apenas ... estranho.

MirroredFate
fonte
Há uma consequência ruim, pelo menos nos idiomas que fornecem um rastreamento completo da pilha. Como o código é fortemente otimizado com muitas inlining, o rastreamento de pilha real e o que um desenvolvedor deseja ver diferem muito e, portanto, a geração de rastreamento de pilha é cara. O uso excessivo de exceções é muito ruim para o desempenho nessas linguagens (Java, C #).
maaartinus
"É uma maneira ruim de fazer as coisas" - isso não deveria ser suficiente para classificá-lo como um antipadrão?
Maybe_Factor
@ Maybe_Factor Pela definição de um padrão de formiga, não.
MirroredFate