Cada Valor Tem um Tipo: Variáveis, Números e Conversões em C#

Cada Valor Tem um Tipo: Variáveis, Números e Conversões em C#

Variáveis e tipos em C#: var e const, a família dos inteiros e o estouro silencioso, checked, float e double contra decimal, por que 0,1 + 0,2 não dá 0,3, valor e referência, casts, o arredondamento bancário, TryParse e a cultura, e o primeiro pedido da Livraria.
Linguagem C#

• • 14 min de leitura

Um número que dá a volta e vira negativo, uma soma de 0,1 com 0,2 que não dá 0,3 e um arredondamento que não segue a regra da escola: os tipos do C# explicam os três.

Na aula anterior, "Do Código-Fonte ao Processo", o programa só escrevia textos fixos. Hoje ele passa a guardar e manipular dados. Em C#, todo dado tem um tipo conhecido pelo compilador. O tipo determina quanto espaço o dado ocupa, quais valores ele aceita e quais operações fazem sentido com ele. Se você vem de Python ou JavaScript, essa é a maior mudança de mentalidade. Se vem do C, vai se sentir em casa, com algumas proteções a mais. E, no fim, a Livraria dá o primeiro passo: vamos calcular o valor de um pedido.

Declarando variáveis

Uma variável é declarada com o tipo seguido do nome:

int paginas = 320;
string titulo = "Dom Casmurro";
bool disponivel = true;

Console.WriteLine(titulo);
Console.WriteLine(paginas);
Console.WriteLine(disponivel);

Diferente do Rust, as variáveis em C# podem ser alteradas por padrão:

int estoque = 10;
estoque = 9;      // vendeu um exemplar

Mas o tipo nunca muda. Depois de declarada como int, a variável só aceita inteiros. estoque = "nove"; é um erro de compilação.

Quando o tipo é óbvio pelo valor, você pode usar var e deixar o compilador inferir:

var autor = "Machado de Assis";   // o compilador deduz: string
var ano = 1899;                   // o compilador deduz: int

Atenção a um equívoco comum: var não torna a variável dinâmica. O tipo é fixado no momento da compilação, exatamente como se você tivesse escrito string ou int. var é só uma forma de não repetir o óbvio. A convenção mais comum é usá-lo quando o tipo fica evidente na mesma linha e escrever o tipo explicitamente quando não fica.

Para valores que nunca mudam, use const:

const int DiasDeGarantia = 30;

A família dos inteiros

O C# tem inteiros de vários tamanhos, com e sem sinal. Diferente do C, o tamanho de cada um é fixo e não depende da máquina:

// tipo      bytes   faixa
// sbyte     1       -128 a 127
// byte      1       0 a 255
// short     2       -32.768 a 32.767
// ushort    2       0 a 65.535
// int       4       cerca de -2,1 bilhões a 2,1 bilhões
// uint      4       0 a cerca de 4,3 bilhões
// long      8       cerca de -9,2 quintilhões a 9,2 quintilhões
// ulong     8       0 a cerca de 18,4 quintilhões

Console.WriteLine(int.MinValue);   // -2147483648
Console.WriteLine(int.MaxValue);   // 2147483647
Console.WriteLine(sizeof(long));   // 8

No dia a dia, int é o padrão para contagens e índices. long entra quando os números podem passar de 2 bilhões: identificadores de banco de dados, tamanhos de arquivos, milissegundos desde 1970. byte aparece em dados binários, como imagens e arquivos. Os demais são raros.

Números longos ficam mais legíveis com o separador _, que o compilador ignora. Também dá para escrever em hexadecimal ou binário:

long populacaoMundial = 8_100_000_000;
int corVermelha = 0xFF0000;
byte flags = 0b0000_0101;

Quando o número não cabe

O que acontece se um int passa do máximo?

int maximo = int.MaxValue;
int estourou = maximo + 1;
Console.WriteLine(estourou);   // -2147483648

O valor dá a volta e vira o menor número possível, sem nenhum aviso. Isso se chama overflow (estouro), e o comportamento padrão do C# é deixá-lo acontecer em silêncio, por desempenho. É a mesma aritmética do processador que você conhece do C, com uma diferença: em C, o estouro de inteiro com sinal é comportamento indefinido; em C#, é definido e sempre dá a volta.

Quando um estouro seria um erro grave (dinheiro, quantidades, cálculos de segurança), peça ao C# para verificar com checked:

int maximo = int.MaxValue;

try
{
    int resultado = checked(maximo + 1);
    Console.WriteLine(resultado);
}
catch (OverflowException)
{
    Console.WriteLine("O cálculo estourou a capacidade de um int.");
}

Agora o estouro lança uma OverflowException em vez de produzir lixo. O try/catch captura esse erro. As exceções ganham uma aula própria no fim desta fase, e por enquanto basta ler como "tente isto; se der este erro, faça aquilo".

Uma curiosidade: se você escrever diretamente int x = int.MaxValue + 1;, o programa nem compila (erro CS0220). Como a expressão é constante, o compilador calcula o resultado na hora, percebe o estouro e recusa. O estouro silencioso só acontece com valores que o compilador não conhece de antemão.

Números com vírgula: float, double e decimal

Para números fracionários, o C# oferece três tipos:

float temperatura = 36.5f;     // 4 bytes, cerca de 7 dígitos de precisão
double distancia = 384_400.5;  // 8 bytes, cerca de 15-16 dígitos: o padrão
decimal preco = 59.90m;        // 16 bytes, 28-29 dígitos, base decimal

Repare nos sufixos: um literal com vírgula decimal é double por padrão. Para float você precisa do f, e para decimal, do m.

float e double seguem o padrão IEEE 754, o mesmo do C e de quase todas as linguagens. Eles guardam os números em base 2, e daí vem a armadilha mais famosa da computação:

double a = 0.1;
double b = 0.2;
Console.WriteLine(a + b);            // 0.30000000000000004
Console.WriteLine(a + b == 0.3);     // False

O problema não é do C#. Assim como 1/3 não tem representação exata em base 10 (0,333...), 0,1 não tem representação exata em base 2. O computador guarda a aproximação mais próxima, e as pequenas diferenças aparecem nas somas.

Para dinheiro, esse erro é inaceitável. Por isso o C# tem o decimal, que guarda os números em base 10:

decimal x = 0.1m;
decimal y = 0.2m;
Console.WriteLine(x + y);            // 0.3
Console.WriteLine(x + y == 0.3m);    // True

A regra prática é simples. Para valores monetários, use decimal, sempre. Para medidas físicas, cálculos científicos, gráficos e jogos, use double, ou float quando o espaço importa, como em jogos e placas de vídeo. O decimal é mais lento, e sua exatidão é desnecessária para uma posição na tela.

O compilador ajuda a não misturar os mundos: decimal preco = 19.90; não compila (erro CS0664), porque 19.90 é um double e a conversão poderia esconder exatamente o erro de precisão que o decimal existe para evitar.

bool, char e string

bool guarda true ou false, e só isso. Diferente do C, inteiros não são booleanos: if (1) não compila.

char guarda um caractere, entre aspas simples, em UTF-16 (2 bytes). Por baixo, é um número:

char letra = 'A';
Console.WriteLine(letra);          // A
Console.WriteLine(letra + 1);      // 66: somar um número ao char produz um int
Console.WriteLine((char)(letra + 1));  // B

string guarda texto, entre aspas duplas. Strings em C# são imutáveis: toda operação que "altera" uma string na verdade cria uma nova. Isso tem consequências de desempenho, que veremos na aula sobre strings.

Valor e referência: uma primeira visita

Existe uma distinção no C# que será central no curso inteiro. Por ora, basta observá-la:

int a = 10;
int b = a;      // b recebe uma CÓPIA do valor de a
b = 20;
Console.WriteLine(a);   // 10: a não foi afetada

int[] x = [1, 2, 3];
int[] y = x;    // y recebe uma cópia da REFERÊNCIA: os dois apontam para o mesmo array
y[0] = 99;
Console.WriteLine(x[0]);   // 99: x "mudou" junto

int, double, bool, char e decimal são tipos de valor: a variável contém o próprio dado, e atribuir copia o dado. Arrays e string são tipos de referência: a variável contém um endereço que aponta para o dado, e atribuir copia o endereço. Se você vem do C, pense em y como um ponteiro que recebeu o mesmo endereço de x, só que sem aritmética de ponteiros e com o coletor de lixo cuidando da liberação.

A string é um caso curioso: é tipo de referência, mas, como é imutável, na prática se comporta como valor. Guarde esta seção. Ela volta com força total quando compararmos class e struct na Fase 2 e quando estudarmos a memória na Fase 4.

Conversões entre tipos

Conversões implícitas acontecem sozinhas quando não há risco de perda:

int quantidade = 1_000;
long grande = quantidade;       // int cabe em long: automático
double real = quantidade;       // int cabe em double: automático

Conversões explícitas (casts) são exigidas quando pode haver perda, e aí a responsabilidade é sua:

double nota = 7.99;
int truncada = (int)nota;
Console.WriteLine(truncada);    // 7: a parte fracionária é DESCARTADA, não arredondada

int grandeDemais = 300;
byte pequeno = (byte)grandeDemais;
Console.WriteLine(pequeno);     // 44: 300 não cabe em byte (máx. 255) e dá a volta: 300 - 256

O cast é você dizendo ao compilador "eu sei o que estou fazendo". Use com atenção.

Para arredondar de verdade, use Math.Round ou Convert. Mas cuidado com uma surpresa:

Console.WriteLine(Math.Round(2.5));         // 2
Console.WriteLine(Math.Round(3.5));         // 4
Console.WriteLine(Convert.ToInt32(2.5));    // 2

Por padrão, o .NET usa o arredondamento bancário: quando o número está exatamente no meio, arredonda para o par mais próximo. Parece estranho, mas evita um viés estatístico. Se sempre arredondássemos 0,5 para cima, somas de muitos valores arredondados cresceriam artificialmente. Se você precisa do arredondamento "da escola", peça explicitamente:

Console.WriteLine(Math.Round(2.5, MidpointRounding.AwayFromZero));   // 3

Transformando texto em número

Tudo que o usuário digita chega como string. Para virar número, é preciso fazer o parse:

int idade = int.Parse("42");   // funciona
int erro = int.Parse("abc");   // lança FormatException: o programa para

Como a entrada do usuário pode ser qualquer coisa, o jeito robusto é o TryParse, que tenta converter e diz se conseguiu, sem lançar exceção:

Console.Write("Digite sua idade: ");
string? entrada = Console.ReadLine();

if (int.TryParse(entrada, out int idade))
    Console.WriteLine($"Ano que vem você terá {idade + 1} anos.");
else
    Console.WriteLine("Isso não é um número inteiro válido.");

Três detalhes. Console.ReadLine() lê uma linha do teclado. O ? em string? indica que o valor pode ser nulo (por exemplo, se a entrada acabar), assunto da Fase 3. E out int idade declara a variável que vai receber o resultado da conversão: o TryParse devolve true ou false e, se deu certo, preenche idade.

Um aviso para quem está no Brasil: números com vírgula dependem da cultura do sistema. Num computador configurado em português, double.Parse("3,14") devolve 3,14, e double.Parse("3.14") devolve 314, sem erro nenhum, porque o ponto é o separador de milhar em português. Em inglês, os papéis se invertem. É uma fonte clássica de bugs em programas que rodam em máquinas diferentes. Veremos como controlar isso quando estudarmos formatação e globalização. Por enquanto, saiba que o problema existe.

Variáveis precisam de valor antes do uso

Em C, ler uma variável local não inicializada devolve lixo de memória. Em C#, o compilador simplesmente não deixa:

int total;
Console.WriteLine(total);   // erro CS0165: uso de variável local não atribuída

É uma classe inteira de bugs eliminada em tempo de compilação.

Juntando tudo: o primeiro pedido da Livraria

Hora de dar o primeiro passo no projeto que vai nos acompanhar pelo curso. Um cliente compra três exemplares de um livro, e a loja dá 10% de desconto em compras a partir de três unidades:

const decimal PrecoUnitario = 59.90m;
const decimal PercentualDesconto = 0.10m;

string titulo = "Dom Casmurro";
int quantidade = 3;

decimal subtotal = PrecoUnitario * quantidade;   // int é convertido para decimal automaticamente
decimal desconto = subtotal * PercentualDesconto;
decimal total = subtotal - desconto;

Console.WriteLine($"Livro:     {titulo}");
Console.WriteLine($"Quantidade: {quantidade}");
Console.WriteLine($"Subtotal:  R$ {subtotal:F2}");
Console.WriteLine($"Desconto:  R$ {desconto:F2}");
Console.WriteLine($"Total:     R$ {total:F2}");

A saída, numa máquina configurada em português, é:

Livro:     Dom Casmurro
Quantidade: 3
Subtotal:  R$ 179,70
Desconto:  R$ 17,97
Total:     R$ 161,73

O :F2 dentro das chaves formata o número com exatamente duas casas decimais. Repare que todos os valores monetários são decimal. Troque tudo para double e rode de novo: com estes números o resultado talvez nem mude, mas agora você sabe que, somando milhares de pedidos, os centavos começariam a divergir. Essa é a diferença entre um sistema que fecha o caixa e um que não fecha.

Ainda há um problema de desenho: o desconto é aplicado sempre, mesmo se a quantidade fosse 1. Para aplicar só a partir de três unidades, o programa precisa tomar decisões. É o assunto de uma aula logo adiante.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Declare variáveis para representar um livro da Livraria: título, autor, número de páginas, preço e se está disponível. Escolha o tipo mais adequado para cada uma e imprima todas.

Ver resposta

✓ Resposta:

string titulo = "Memórias Póstumas de Brás Cubas";
string autor = "Machado de Assis";
int paginas = 256;
decimal preco = 44.90m;
bool disponivel = true;

Console.WriteLine($"{titulo}, de {autor}");
Console.WriteLine($"{paginas} páginas, R$ {preco:F2}");
Console.WriteLine($"Disponível: {disponivel}");

O ponto central é o preço em decimal (com o sufixo m), e não em double. Para o número de páginas, int é adequado. short também caberia, mas a economia de dois bytes não compensa a inconveniência: int é o padrão para inteiros.

Exercício 2

Sem rodar, preveja o que este código imprime. Depois rode e confira.

byte b = 255;
b++;
Console.WriteLine(b);
Ver resposta

✓ Resposta: Imprime 0. O byte vai de 0 a 255. Somar 1 a 255 estoura a capacidade, e, como o C# não verifica estouro por padrão, o valor dá a volta para 0. Se a linha fosse checked { b++; }, o programa lançaria uma OverflowException. Repare que o compilador não reclamou: ele não sabe, ao compilar, que b vai valer 255 naquele ponto. O estouro silencioso só é detectado por você, pelo checked ou por um teste.

Exercício 3

O código abaixo não compila. Explique por quê e corrija.

decimal preco = 19.90;
Ver resposta

✓ Resposta: O literal 19.90 sem sufixo é um double. O C# não converte double para decimal implicitamente, porque os dois têm representações diferentes (base 2 contra base 10) e a conversão poderia carregar erros de arredondamento para dentro do decimal, que existe justamente para evitá-los. O compilador acusa o erro CS0664. A correção é usar o sufixo m: decimal preco = 19.90m;.

Exercício 4

Preveja a saída de cada linha e explique cada resultado:

Console.WriteLine((int)7.9);
Console.WriteLine(Convert.ToInt32(7.5));
Console.WriteLine(Convert.ToInt32(6.5));
Console.WriteLine(Math.Round(6.5, MidpointRounding.AwayFromZero));
Ver resposta

✓ Resposta: A saída é 7, 8, 6 e 7. O cast (int)7.9 trunca: descarta a parte fracionária sem arredondar. Convert.ToInt32 arredonda, mas usa o arredondamento bancário: 7,5 está exatamente no meio entre 7 e 8, e vai para o par, 8. Pela mesma regra, 6,5 fica entre 6 e 7 e vai para o par, 6. Por fim, com MidpointRounding.AwayFromZero, o meio sempre se afasta do zero, então 6,5 vira 7, como na escola. A lição: truncar e arredondar são operações diferentes, e o arredondamento padrão do .NET não é o que a maioria das pessoas espera.

Exercício 5

Escreva um programa que peça dois números inteiros ao usuário e imprima a soma. Se algum deles não for um número válido, o programa deve avisar e terminar com código de saída 1. Garanta que a soma de dois valores grandes, como 2.000.000.000 + 2.000.000.000, dê o resultado correto.

Ver resposta

✓ Resposta:

Console.Write("Primeiro número: ");
if (!long.TryParse(Console.ReadLine(), out long a))
{
    Console.Error.WriteLine("Valor inválido.");
    return 1;
}

Console.Write("Segundo número: ");
if (!long.TryParse(Console.ReadLine(), out long b))
{
    Console.Error.WriteLine("Valor inválido.");
    return 1;
}

Console.WriteLine($"Soma: {a + b}");
return 0;

Três decisões merecem comentário. Primeiro, o TryParse evita que uma entrada inválida derrube o programa com uma exceção. Segundo, o ! inverte o resultado: "se não conseguiu converter". Os operadores lógicos são tema da próxima aula. Terceiro, e mais importante: com int, a soma de 2.000.000.000 + 2.000.000.000 passaria do máximo de cerca de 2,1 bilhões e daria a volta, produzindo um número negativo sem nenhum aviso. Usar long, que vai até cerca de 9,2 quintilhões, resolve o caso pedido. Se as entradas pudessem ser ainda maiores, a próxima defesa seria checked(a + b) para detectar o estouro em vez de devolver lixo.

Comentários

Mais em Linguagem C#

Áudio, interface e save game: o que faz um jogo parecer pronto
Áudio, interface e save game: o que faz um jogo parecer pronto

O que faz a Biblioteca em Apuros parecer pronta: barramentos de áudio e volume…

Assincronia Avançada: Muitas Tarefas, Cancelamento e Fluxos Assíncronos
Assincronia Avançada: Muitas Tarefas, Cancelamento e Fluxos Assíncronos

Assincronia avançada no C#: Task.WhenAll e as exceções múltiplas, WaitAsync e…

Do Código-Fonte ao Processo: Instalando o .NET e Entendendo o que Acontece ao Rodar
Do Código-Fonte ao Processo: Instalando o .NET e Entendendo o que Acontece ao Rodar

Instalar o .NET 10 e entender o caminho do código até o processador: SDK e…