← C#: linguagem e decisões de aplicação
01 / 10 · 45 MIN

Tipos, conversões e limites numéricos

Segue os tipos de cada operação, preserva limites numéricos e distingue ausência, parsing e validação.

O tipo pertence à expressão

Em C#, var não significa que uma variável muda de tipo em cada atribuição. Com var attempts = 6, o compilador infere int. Uma atribuição posterior de "6" não transforma essa variável numa string nem faz parsing automaticamente. A representação de origem e a representação de destino continuam a ter um contrato.

O percurso usa C# 14 com .NET 10, sem funcionalidades preview. A linguagem define as operações disponíveis; o framework de destino determina os tipos e APIs que o programa pode usar. Não é preciso instalar um SDK para responder às avaliações: os excertos são interpretados por leitura. ASP.NET Core, rotas HTTP e aplicações web pertencem a outro percurso.

Num diagnóstico, anota primeiro o tipo de cada operando e só depois o tipo do resultado. A existência de uma variável long ou double à esquerda de uma atribuição não faz todas as operações à direita nesse tipo.

Converter não é validar

Uma conversão implícita permite usar, por exemplo, um int onde se espera long. Nem toda a conversão implícita promete preservar todos os dígitos: int para float pode perder precisão. Uma conversão explícita torna a escolha visível, mas também pode perder informação ou falhar.

Um byte representa valores de 0 a 255. Se uma variável int contém 300, checked((byte)value) rejeita a conversão com OverflowException. unchecked((byte)value) conserva os oito bits inferiores e produz 44. Isso é diferente de limitar a entrada a 255. Se 300 representa um erro de configuração, wraparound não é uma política de recuperação.

Constantes têm regras próprias. byte level = 12 compila porque o literal constante cabe no destino; byte level = 300 não compila. Não transfiras essa autorização automaticamente para uma variável int só porque o valor observado hoje é pequeno.

A ordem das conversões muda a conta

Com int ok = 9 e int seen = 20, a expressão ok / seen faz divisão inteira e produz zero. Guardar esse resultado em double conserva zero. Para obter a fração, (double)ok / seen converte um operando antes da divisão. Já (double)(ok / seen) chega tarde: o resultado inteiro perdeu a parte fracionária.

O mesmo raciocínio aplica-se à capacidade. Se dois int são multiplicados, o produto é calculado em int. Num lote com 80 000 unidades de 40 000 bytes, long bytes = units * size não evita o excesso na multiplicação intermédia. long bytes = (long)units * size calcula em long e preserva o total de 3 200 000 000.

Escolher long não dispensa limites. Se os fatores já fossem long muito grandes, a multiplicação poderia também exceder long. A capacidade deve ser justificada a partir dos limites das entradas, não apenas por um teste com valores pequenos.

Contexto checked e intenção decimal

checked aplica-se às operações escritas dentro do seu âmbito textual. Uma chamada checked(Add(x)) não altera o corpo de Add. Se esse corpo usa unchecked(x + 1), a adição continua unchecked. Coloca a regra no cálculo que precisas de controlar e distingue um erro de constante na compilação de uma exceção com valores de runtime.

Para valores decimais, o sufixo m expressa a escolha diretamente: decimal rate = 0.075m. O literal 0.075 sem sufixo é double e não se converte implicitamente para decimal. Misturar decimal e double exige uma decisão explícita sobre representação. Não se deve acrescentar casts apenas até o compilador aceitar a conta.

O cast de decimal para int trunca na direção de zero. (int)(-6.8m) produz -6, não -7. Se o negócio exige arredondamento por uma regra específica, essa regra tem de ser implementada de propósito. Precisão monetária e modos de arredondamento serão aprofundados no módulo de contratos de dados.

Ausência não é zero

int? pode representar um int ou ausência. Em int? retries = null, HasValue é false e ler Value lança InvalidOperationException. A expressão retries ?? 2 devolve 2 sem alterar retries. É adequada apenas se duas tentativas forem a política explícita para uma configuração ausente.

As comparações ordenadas têm uma armadilha: com int? reading = null, reading < 10 e reading >= 10 são ambas false. Uma decisão que use apenas esses dois ramos pode deixar a ausência sem tratamento. Decide primeiro o que fazer quando a leitura falta e só depois compara um valor presente.

Também não confundas ausência com zero observado. Numa contagem de stock, zero pode ser uma medição válida; num pedido opcional, null pode significar "não alterar". Converter ambos para zero demasiado cedo elimina informação de que a operação seguinte precisa.

Caso guiado: aceitar uma linha de stock

Um importador recebe quantidades em texto e aceita valores entre 0 e 500. Tem de separar três resultados: texto que não se converte em int, inteiro fora do intervalo e linha aceite. Um campo numérico sozinho não transporta esses três resultados.

O excerto usa TryParse para obter uma decisão de conversão e devolve reason com "parse-failed" ou "out-of-range" nos dois tipos de rejeição. Só consulta o intervalo depois de essa decisão ser verdadeira. "0" é aceite como zero; "26" é aceite como 26; "800" é recusado pelo limite; "many" é recusado pelo parsing. Uma instrução posterior só pode aplicar stock se TryQuantity devolver true. O out quantity fica zero na falha deste wrapper por escolha explícita, mas esse zero não é uma aprovação.

O exemplo recebe inteiros simples e usa o overload normal de TryParse. Um protocolo que imponha separadores ou cultura própria precisa de especificar essas regras através de overloads apropriados. Não se infere aqui que todo o texto numérico de qualquer sistema tenha o mesmo formato.

Revisão antes da decisão

Para rever um cálculo, escreve os tipos das entradas, o tipo de cada operação intermédia, os limites possíveis e o efeito de uma falha. Se a conversão está depois da perda de precisão ou do overflow, move a decisão para o operando certo. Se a entrada pode faltar, não uses uma regra de valor como prova de presença.

No trabalho, estas distinções surgem em contadores, limites de capacidade, importações e configurações. Um diagnóstico útil identifica onde a informação foi perdida: parsing, cálculo, conversão ou valor alternativo. A correção deve conservar a regra de negócio e permitir distinguir dados inválidos de valores legítimos.

Resumo: var infere um tipo; um cast não é validação; checked tem âmbito textual; int? representa ausência; TryParse devolve uma decisão que deve ser respeitada. Os próximos módulos usam estas regras em parâmetros, coleções e contratos de dados.

static bool TryQuantity(string? text, out int quantity,
    out string? reason)
{
    quantity = 0;
    reason = null;
    if (!int.TryParse(text, out int parsed))
    {
        reason = "parse-failed";
        return false;
    }
    if (parsed < 0 || parsed > 500)
    {
        reason = "out-of-range";
        return false;
    }
    quantity = parsed;
    return true;
}
NA PRÁTICA

Método para ler dentro de uma classe, com contexto nullable ativado. O booleano indica aceitação; reason distingue as rejeições e é null no sucesso. quantity só deve ser usado como dado aceite quando o resultado é true. Não escreve no sistema de stock.

Armadilhas comuns

Confiar no tipo do destino para evitar overflow; converter depois da divisão; ignorar o booleano de TryParse; tratar null como zero; usar checked apenas no chamador.

Tópicos relacionados: Referências, nullability e texto · Métodos, parâmetros e padrões · Decimais, tempo e contratos de dados

Leva esta ideia contigo

Preserva a informação antes de calcular, converter ou aplicar valores alternativos.

Criar conta

Referência: Built-in numeric conversions · C# 14 / .NET 10; no preview features