Skip to content

Posts

Examinando o Caso para Instruções Switch

6 de dezembro de 2015 • 10 min de leitura

Examinando o Caso para Instruções Switch

Por quase 50 anos, a instrução switch (também conhecida como instrução case) tem sido uma parte integral da programação. Nos últimos anos, porém, alguns estão afirmando que a instrução switch perdeu sua utilidade. Outros vão ainda mais longe, rotulando a instrução switch como um code-smell.

Em 1952, Stephen Kleene concebeu a instrução switch em seu artigo, Introduction to Metamathematics. A primeira implementação notável foi em ALGOL 58 em 1958. Posteriormente, a instrução switch foi incluída na indelével linguagem de programação C, que, como sabemos, influenciou a maioria das linguagens de programação modernas.

Avançando para os dias atuais e praticamente todas as linguagens têm uma instrução switch. No entanto, algumas linguagens omitiram a instrução switch. A mais notável sendo Smalltalk.

Isso despertou minha curiosidade, por que a instrução switch foi excluída do Smalltalk?

Andy Bower, um dos criadores/proponentes por trás do Dolphin Smalltalk compartilhou seus pensamentos sobre por que Smalltalk excluiu a instrução switch:

Quando cheguei ao Smalltalk vindo de C++, não conseguia entender como uma linguagem supostamente completa não suportava uma construção switch/case. Afinal, quando primeiro passei de BASIC para “programação estruturada”, pensei que switch era uma das melhores coisas desde o pão fatiado. No entanto, como Smalltalk não suportava switch, tive que procurar e entender como superar essa deficiência. A resposta correta é, é claro, usar polimorfismo e fazer com que os próprios objetos façam dispatch para a peça correta de código. Então percebi que não era uma “deficiência” em tudo, mas Smalltalk estava me forçando a um design OOP muito mais granular do que eu tinha me acostumado em C++. Se houvesse uma instrução switch disponível, teria levado muito mais tempo para aprender isso ou, pior, eu ainda poderia estar programando em estilo pseudo-objeto C++/Java em Smalltalk.
Eu argumentaria que em OOP normal não há necessidade real de uma instrução switch. Às vezes, ao fazer interface com um mundo não-OOP (como receber e fazer dispatch de mensagens WM_XXXX do Windows que não são objetos mas apenas inteiros), então uma instrução switch seria útil. Nessas situações, existem alternativas (como fazer dispatch de um Dictionary) e o número de vezes que aparecem não justifica a inclusão de sintaxe adicional.

Andy estava certo? Estamos melhor sem a instrução switch? Outras linguagens também se beneficiariam ao excluir a instrução switch?

Para esclarecer essa questão, reuni uma comparação entre uma instrução switch, um dicionário e polimorfismo. Vamos chamá-lo de confronto. Que a melhor implementação vença!

Cada implementação tem um método que recebe um parâmetro, um inteiro, e retorna uma string. Usaremos complexidade ciclomática e índice de manutenibilidade para examinar cada implementação. Então tomaremos uma visão holística de todas as três implementações.

O código.

Instrução Switch

Índice de Manutenibilidade72
Complexidade Ciclomática6
    public class SwitchWithFourCases
    {
        public string SwitchStatment(int color)
        {
            var colorString = "Red";

            switch (color)
            {
                case 1:
                    colorString = "Green";
                    break;

                case 2:
                    colorString = "Blue";
                    break;

                case 3:
                    colorString = "Violet";
                    break;

                case 4:
                    colorString = "Orange";
                    break;

            }

            return colorString;
        }
    }

Dicionário

Índice de Manutenibilidade73
Complexidade Ciclomática3
public class DictionaryWithFourItems
{ 
    public string Dictionary(int color)
    {
        var colorString = "Red";
        var colors = new Dictionary<int, string> {{1, "Green"}, {2, "Blue"}, {3, "Violet"}, {4, "Orange"}};
        var containsKey = colors.ContainsKey(color);
        if (containsKey)
        {
            colorString = colors[color];
        }

        return colorString;
    }
}

Polimorfismo

Índice de Manutenibilidade Total94
Complexidade Ciclomática Total15

Interface

Índice de Manutenibilidade100
Complexidade Ciclomática1
public interface IColor
{
    string ColorName { get; }
}

Factory

Índice de Manutenibilidade76
Complexidade Ciclomática4
public class ColorFactory
{
    public string GetColor(int color)
    {
        IColor defaultColor = new RedColor();
        var colors = GetColors();
        var containsKey = colors.ContainsKey(color);
        if (containsKey)
        {
            var c = colors[color];
            return c.ColorName;
        }

        return defaultColor.ColorName;
    }

    private static IDictionary<int, IColor> GetColors()
    {
        return new Dictionary<int, IColor>
        {
            {1, new GreenColor()}, 
            {2, new BlueColor()}, 
            {3, new VioletColor()}, 
            {4, new OrangeColor()}, 
            {5, new MagentaColor()}
        };
    }
}

Implementação

Índice de Manutenibilidade97
Complexidade Ciclomática2
public class BlueColor : IColor
{
    public string ColorName => "Blue";
}

public class RedColor : IColor
{
    public string ColorName => "Red";
}

public class GreenColor : IColor
{
    public string ColorName => "Green";
}

public class MagentaColor : IColor
{
    public string ColorName => "Magenta";
}

public class VioletColor : IColor
{
    public string ColorName => "Violet";
}

Os Resultados

Antes de mergulhar nos resultados, vamos definir Complexidade Ciclomática e Índice de Manutenibilidade:

  • Complexidade Ciclomática é a medida de ramificação lógica. Quanto menor o número, melhor.
  • Índice de Manutenibilidade mede a manutenibilidade do código. Está em uma escala entre 0 e 100. Quanto maior o número, melhor.
Complexidade CiclomáticaÍndice de Manutenibilidade
Instrução Switch672
Dicionário373
Polimorfismo1594

Vamos examinar a complexidade ciclomática primeiro.

Os resultados para complexidade ciclomática são diretos. A implementação de dicionário é a mais simples. Isso significa que é a melhor solução? Não, como veremos ao avaliar o índice de manutenibilidade.

A maioria pensaria como eu, a implementação com a menor complexidade ciclomática é a mais mantível — como poderia ser de outra forma?

Em nosso cenário, a implementação com a menor complexidade ciclomática não é a mais mantível. Na verdade, em nosso cenário, é o oposto. A implementação mais complexa é a mais mantível! Mente explodida!

Se você se lembra, quanto maior a pontuação do índice de manutenibilidade, melhor. Cortando para o ponto, polimorfismo tem a melhor pontuação do índice de manutenibilidade — mas também tem a maior complexidade ciclomática. O que está acontecendo? Isso não parece certo.

Por que a implementação mais complexa é a mais mantível? Para responder isso, devemos entender o índice de manutenibilidade.

O índice de manutenibilidade consiste em 4 métricas: complexidade ciclomática, linhas de código, número de comentários e volume de Halstead. As três primeiras métricas são relativamente bem conhecidas, mas a última, o Volume de Halstead, é relativamente desconhecida. Como a complexidade ciclomática, o Volume de Halstead tenta medir objetivamente a complexidade do código.

Em termos simples, o Volume de Halstead mede o número de partes móveis (variáveis, chamadas de sistema, aritmética, construções de código, etc.) no código. Quanto maior o número de partes móveis, mais complexidade. Quanto menor o número de partes móveis, menor a complexidade. Isso explica por que a implementação polimórfica pontua alto no índice de manutenibilidade; as classes têm pouco ou nenhuma parte móvel. Outra forma de olhar para o Volume de Halstead é que ele mede a densidade de “partes móveis”.

O que é software, se não for para mudar? Para refletir o mundo real, estamos introduzindo mudança. Adicionei uma nova cor a cada implementação.

Abaixo estão os resultados revisados.

Complexidade CiclomáticaÍndice de Manutenibilidade
Instrução Switch770
Dicionário373
Polimorfismo1795

As abordagens de instrução switch e polimórfica aumentaram em complexidade ciclomática em uma unidade, mas curiosamente, o dicionário não aumentou. A princípio fiquei confuso com isso, mas então percebi que o dicionário considera as cores como dados e as outras duas implementações tratam as cores como código. Vou ir direto ao ponto.

Voltando nossa atenção para o índice de manutenibilidade, apenas um, a instrução switch, diminuiu em manutenibilidade. A pontuação de manutenibilidade do Polimorfismo melhorou e ainda assim a complexidade também aumenta (preferiríamos que diminuísse). Como mencionei acima, isso é contra-intuitivo.

Nossa comparação mostra que dicionários podem, do ponto de vista da complexidade, escalar infinitamente. A abordagem polimórfica é de longe a mais mantível e parece aumentar em manutenibilidade conforme mais cenários são adicionados. A instrução switch aumenta em complexidade e diminui em manutenibilidade quando o novo cenário foi adicionado. Mesmo antes de adicionarmos o novo cenário, tinha as piores medidas de complexidade ciclomática e índice de manutenibilidade.

Jem Finch do Google compartilhou seus pensamentos sobre as deficiências das instruções switch:

1. As implementações de métodos polimórficos são lexicalmente isoladas umas das outras. Variáveis podem ser adicionadas, removidas, modificadas, e assim por diante, sem qualquer risco de impactar código não relacionado em outro ramo da instrução switch.

2. As implementações de métodos polimórficos são garantidas de retornar ao lugar correto, assumindo que terminam. Instruções switch em uma linguagem com fall through como C/C++/Java requerem uma instrução “break” propensa a erros para garantir que retornem à instrução após o switch em vez do próximo bloco case.

3. A existência de uma implementação de método polimórfico pode ser imposta pelo compilador, que se recusará a compilar o programa se uma implementação de método polimórfico estiver faltando. Instruções switch não fornecem tal verificação de exaustividade.

4. O dispatch de método polimórfico é extensível sem acesso a (ou recompilação de) outro código-fonte. Adicionar outro case a uma instrução switch requer acesso ao código de dispatch original, não apenas em um lugar, mas em todos os lugares onde o enum relevante está sendo alternado.

5. … você pode testar métodos polimórficos independentemente do aparato de switching. A maioria das funções que alternam como o exemplo que o autor deu conterá outro código que não pode ser testado separadamente; chamadas de método virtual, por outro lado, podem.

6. As chamadas de método polimórfico garantem dispatch em tempo constante. Nenhum compilador suficientemente inteligente é necessário para converter o que é naturalmente uma construção de tempo linear (a instrução switch com fall through) em uma construção de tempo constante.

Infelizmente, ou felizmente, dependendo do seu lado, a maioria das linguagens tem uma instrução switch, e elas não vão desaparecer tão cedo. Com isso em mente, é bom saber o que está acontecendo nos bastidores ao compilar instruções switch.

Existem três otimizações de instrução switch que podem ocorrer:

  1. Instruções If-elseif – Quando uma instrução switch tem um pequeno número de cases ou cases esparsos (valores não incrementais, como 10, 250, 1000), ela é convertida em uma instrução if-elseif.
  2. Tabela de Salto – Em conjuntos maiores de cases adjacentes (1, 2, 3, 4, 5), o compilador converte a instrução switch em uma tabela de salto. Uma Tabela de Salto é essencialmente uma Hashtable com um ponteiro (pense em instrução goto) para a função na memória.
  3. Busca Binária – Para grandes conjuntos de cases esparsos, o compilador pode implementar uma busca binária para identificar o case rapidamente, semelhante a como um índice funciona em um banco de dados. Em casos extraordinários onde cases são um grande número de cases esparsos e adjacentes, o compilador usará uma combinação das três otimizações.

Resumo

Em um mundo orientado a objetos, a instrução switch, concebida em 1952, é uma base do engenheiro de software. Uma exceção notável é Smalltalk, onde os designers optaram por excluir a instrução switch.

Quando comparada a implementações equivalentes alternativas, o dicionário e o polimorfismo, a instrução switch não se saiu tão bem.

A instrução switch está aqui para ficar, mas como nossa comparação mostrou, existem melhores alternativas para a instrução switch.

As implementações estão disponíveis no Github.

Autor: Chuck Conway é um Engenheiro de IA com quase 30 anos de experiência em engenharia de software. Ele constrói sistemas de IA práticos—pipelines de conteúdo, agentes de infraestrutura e ferramentas que resolvem problemas reais—e compartilha o que está aprendendo ao longo do caminho. Conecte-se com ele nas redes sociais: X (@chuckconway) ou visite-o no YouTube e no SubStack.

↑ Voltar ao topo

Você também pode gostar