C# float, double e decimal

Altura, peso, temperatura, preço, média de notas: muita coisa tem vírgula. O C# tem três tipos para esses números: double, float e decimal. Os dois primeiros são rápidos, mas guardam uma surpresa que pega todo iniciante: 0.1 + 0.2 não dá exatamente 0.3. O terceiro é exato, e é por isso que dinheiro em C# é sempre decimal. Nesta aula você vai ver os três, lado a lado.

Você vai aprender
  • double (o padrão), float e decimal, e quantos dígitos cada um acerta.
  • Os sufixos f e m, e o erro CS0664 quando eles faltam.
  • Por que 0.1 + 0.2 dá 0,30000000000000004 com double e 0,3 com decimal.
  • Por que dinheiro é sempre decimal, e por que não dá para misturar decimal com double (CS0019).
  • A notação científica (1.5e3, 1E+20).
  • A divisão por zero: ∞ e NaN com double, DivideByZeroException com decimal e int.

Todos os programas desta aula foram rodados com o .NET 10 (C# 14) na máquina fictícia da Ana (usuária ana, computador notebook, Ubuntu em português), na pasta ~/cs, com dotnet run arquivo.cs. Por isso as saídas usam vírgula: 3,5.


A ideia: a dízima que não cabe no papel

Tente escrever 1/3 em decimal numa folha: 0,33333…. Os 3 não acabam nunca. Em algum momento você para e aceita um valor quase certo.

O double e o float fazem contas em binário (base 2), e em binário acontece a mesma coisa com números que, para nós, são simples: 0,1 em binário é uma dízima infinita (0,000110011001100…). Como a caixa tem um número fixo de bits, ela guarda o valor mais próximo que cabe. Quase sempre ninguém percebe; às vezes, a diferença aparece.

O decimal faz as contas em base 10, como nós. Para ele, 0,1 é exatamente 0,1. Ele é mais lento e ocupa o dobro do espaço, mas não erra os centavos.


1. Os três tipos

float f = 1.0f / 3;
double d = 1.0 / 3;
decimal m = 1.0m / 3;
Console.WriteLine(f);
Console.WriteLine(d);
Console.WriteLine(m);
$ dotnet run tres.cs
0,33333334
0,3333333333333333
0,3333333333333333333333333333
float f = 1.0f / 3;
float ("flutuante", de ponto flutuante): 4 bytes, uns 7 dígitos certos. O f no fim do número diz "este número é float". Veja o último dígito: …34, o float já errou.
double d = 1.0 / 3;
double ("duplo", de precisão dupla): 8 bytes, uns 15 a 17 dígitos. É o padrão: um número com ponto no código, sem sufixo, como 1.0, é double.
decimal m = 1.0m / 3;
decimal: 16 bytes, 28 a 29 dígitos, em base 10. O m (de money, "dinheiro") diz "este número é decimal".
/ 3
A divisão. O 3 é int, mas, numa conta com um float, double ou decimal, ele vira desse tipo sozinho. Se os dois lados fossem int, 1 / 3 daria 0 (divisão inteira, aula Operadores Aritméticos).
TipoBytesDígitos certosBaseSufixoUse para
float4~72f ou FGráficos e jogos (Unity), muitos dados quando a memória pesa
double8~15 a 172nenhum (ou d)Medidas: altura, peso, distância, ciência, médias
decimal1628 a 2910m ou MDinheiro e tudo que precisa ser exato em base 10

Os sufixos são obrigatórios

Como 62.5 sem sufixo é double, ele não entra direto numa caixa float nem decimal:

float peso = 62.5;
Console.WriteLine(peso);
$ dotnet run floatlit.cs
/home/ana/cs/floatlit.cs(1,14): error CS0664: Literal do tipo double não pode ser convertido implicitamente no tipo "float"; use um sufixo "F" para criar um literal desse tipo

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
decimal preco = 19.90;
Console.WriteLine(preco);
$ dotnet run declit.cs
/home/ana/cs/declit.cs(1,17): error CS0664: Literal do tipo double não pode ser convertido implicitamente no tipo "decimal"; use um sufixo "M" para criar um literal desse tipo

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

O próprio erro diz o conserto: 62.5f e 19.90m. O C# é rigoroso aqui de propósito: passar de double para float perde dígitos, e passar de double para decimal pode carregar o "quase certo" do double para dentro do seu dinheiro. Já um número inteiro entra sem sufixo: decimal preco = 10; funciona, porque 10 é exato em qualquer tipo.

Missão: o preço da camiseta não compila. Rode para ver o erro CS0664 e conserte com o sufixo certo, para mostrar Preço: 19,90.

decimal preco = 19.90;
Console.WriteLine("Preço: " + preco);
decimal preco = 19.90m;
Console.WriteLine("Preço: " + preco);

Repare que o decimal guarda o zero do fim: 19.90m aparece como 19,90. O double 19.90 apareceria como 19,9.


2. A surpresa: 0.1 + 0.2

double a = 0.1 + 0.2;
decimal b = 0.1m + 0.2m;
Console.WriteLine(a);
Console.WriteLine(b);
Console.WriteLine(a == 0.3);
Console.WriteLine(b == 0.3m);
$ dotnet run soma01.cs
0,30000000000000004      « double: quase 0,3
0,3                      « decimal: exato
False                    « para o double, 0.1 + 0.2 não é igual a 0.3
True
double a = 0.1 + 0.2;
Nem 0.1 nem 0.2 são exatos em binário. A soma dos dois "quase" deu um "quase" um pouquinho maior: 0,30000000000000004.
decimal b = 0.1m + 0.2m;
Em base 10, 0,1 e 0,2 são exatos, e a soma é exatamente 0,3.
a == 0.3
O == pergunta "é igual?" (aula Operadores de Comparação). Para o double, a resposta é False: os dois números diferem na 17ª casa.

Isso não é defeito do C#. Acontece igual em Java, Python, JavaScript, C e na planilha: todos usam o mesmo padrão de double (o IEEE 754). O .NET só mostra o número com todos os dígitos que ele realmente tem.

Por que dinheiro é sempre decimal

A lojinha da Ana vende balas de R$ 0,10. Ela soma 10 balas no caixa, uma por uma:

double totalD = 0;
decimal totalM = 0;
for (int i = 0; i < 10; i++)
{
    totalD = totalD + 0.10;
    totalM = totalM + 0.10m;
}
Console.WriteLine(totalD);
Console.WriteLine(totalM);
Console.WriteLine(totalD == 1.0);
$ dotnet run dez.cs
0,9999999999999999       « double: faltou um "pedacinho" de centavo
1,00                     « decimal: R$ 1,00 certinho
False

O for repete a soma 10 vezes (aula for). Com double, cada soma erra um tiquinho, e os erros se acumulam: no fim, o caixa tem R$ 0,9999999999999999. Num banco, com milhões de contas e operações, esses tiquinhos viram dinheiro de verdade. E um if (total == 1.0) nunca seria verdadeiro. Com decimal, dez vezes 0,10 é 1,00.

Regra do guia: dinheiro é decimal

Preço, saldo, salário, troco, juros: sempre decimal, com o sufixo m nos números (19.90m). Use double para medidas e ciência, em que um erro na 16ª casa não importa.

Uma conta de dinheiro completa

decimal preco = 19.90m;
int quantidade = 3;
decimal total = preco * quantidade;
Console.WriteLine(total);
Console.WriteLine(total.ToString("C"));
decimal desconto = total * 0.1m;
Console.WriteLine(desconto);
Console.WriteLine(total - desconto);
Console.WriteLine(Math.Round(total - desconto, 2));
$ dotnet run dinheiro.cs
59,70
R$ 59,70
5,970
53,730
53,73
preco * quantidade
decimal vezes int dá decimal. O resultado guarda as duas casas do preço: 59,70.
total.ToString("C")
O formato "C" (currency, "moeda") mostra como dinheiro do país do computador: R$ 59,70. Outros formatos em Formatar Números.
total * 0.1m
10% de desconto. O decimal soma as casas dos dois números (2 + 1), e o resultado tem 3: 5,970. O valor está certo; só aparece com um zero a mais.
Math.Round(…, 2)
Arredonda para 2 casas: 53,73. A classe Math tem aula própria: Matemática.

3. Não dá para misturar decimal e double

A Ana quer pôr 10% de taxa num preço e escreve 1.1, sem o m:

decimal preco = 100m;
decimal comTaxa = preco * 1.1;
Console.WriteLine(comTaxa);
$ dotnet run misturadec.cs
/home/ana/cs/misturadec.cs(2,19): error CS0019: O operador "*" não pode ser aplicado a operandos dos tipos "decimal" e "double"

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

O int se mistura com qualquer um (ele é exato), mas decimal e double não se misturam sozinhos: o C# não sabe qual dos dois você quer, e qualquer escolha poderia perder algo. O conserto é escrever 1.1m. Se o valor já está numa variável double, converta de propósito: preco * (decimal)taxa (aula Conversão de Tipos).


4. Limites e notação científica

Console.WriteLine(float.MaxValue);
Console.WriteLine(double.MaxValue);
Console.WriteLine(decimal.MaxValue);
Console.WriteLine(double.Epsilon);
Console.WriteLine(1.5e3);
Console.WriteLine(2.5e-4);
Console.WriteLine(1e20);
Console.WriteLine(123456789.123456789);
Console.WriteLine(123456789.123456789f);
$ dotnet run limitesdec.cs
3,4028235E+38
1,7976931348623157E+308
79228162514264337593543950335
5E-324
1500
0,00025
1E+20
123456789,12345679
123456790
3,4028235E+38
Notação científica: E+38 quer dizer "vezes 10 elevado a 38". O maior float é 3,4 seguido de 38 casas. O maior double tem 308 casas!
79228162514264337593543950335
O maior decimal, uns 79 octilhões. Bem menor que o do double: o decimal troca alcance por exatidão.
5E-324
double.Epsilon, o menor double positivo: 5 × 10-324. E- é "dividido por 10 elevado a".
1.5e3 · 2.5e-4 · 1e20
Você também pode escrever números assim no código: 1.5e3 = 1,5 × 1000 = 1500; 2.5e-4 = 0,00025. O .NET passa a mostrar em notação científica quando o número fica grande demais (1E+20).
123456789,12345679 · 123456790
O mesmo número em double e em float. O double acertou 17 dígitos; o float, só 8, e arredondou o resto.

5. Divisão por zero

Dividir por zero não dá o mesmo resultado nos três tipos (nem no int).

Com double: infinito e NaN

double a = 1.0 / 0;
double b = -1.0 / 0;
double c = 0.0 / 0;
Console.WriteLine(a);
Console.WriteLine(b);
Console.WriteLine(c);
Console.WriteLine(double.IsInfinity(a));
Console.WriteLine(double.IsNaN(c));
Console.WriteLine(c == double.NaN);
$ dotnet run divzero.cs
∞
-∞
NaN
True
True
False
1.0 / 0 → ∞
O double tem um valor especial para "infinito". O .NET mostra o símbolo ∞, em pt-BR e também em en-US; só a cultura invariante (CultureInfo.InvariantCulture, aula Cultura) escreve Infinity. O programa não para.
-1.0 / 0 → -∞
Infinito negativo.
0.0 / 0 → NaN
Not a Number, "não é um número": uma conta sem resultado. Aparece como NaN também em pt-BR.
double.IsInfinity · double.IsNaN
Perguntam "é infinito?" e "é NaN?".
c == double.NaN → False
Pegadinha: NaN não é igual a nada, nem a ele mesmo. Para saber se um número é NaN, use sempre double.IsNaN.

O perigo é que um ∞ ou um NaN "contamina" as contas seguintes (NaN + 1 é NaN) e o programa segue em frente mostrando resultados sem sentido. Confira o divisor antes de dividir.

Com decimal e int: o programa para

decimal saldo = 100m;
decimal pessoas = 0m;
Console.WriteLine(saldo / pessoas);
$ dotnet run divzerom.cs
Unhandled exception. System.DivideByZeroException: Attempted to divide by zero.
   at System.Decimal.DecCalc.VarDecDiv(DecCalc& d1, DecCalc& d2)
   at System.Decimal.op_Division(Decimal d1, Decimal d2)
   at Program.<Main>$(String[] args) in /home/ana/cs/divzerom.cs:line 3

O decimal não tem infinito nem NaN. Dividir por zero lança a exceção System.DivideByZeroException: "tentativa de dividir por zero". As duas primeiras linhas at são de dentro do próprio .NET (a divisão de decimal); a última é a que interessa: divzerom.cs, linha 3. O int faz o mesmo (veja Operadores Aritméticos). Para dinheiro isso é bom: melhor parar do que dividir uma conta por zero pessoas e seguir.

Contadoubledecimalint
1 / 0∞DivideByZeroExceptionDivideByZeroException
-1 / 0-∞DivideByZeroExceptionDivideByZeroException
0 / 0NaNDivideByZeroExceptionDivideByZeroException

6. Comparar números double

Como o double é "quase certo", nunca compare com == um resultado de conta. Pergunte se a diferença é pequena o bastante (uma tolerância):

double x = 0.1 + 0.2;
Console.WriteLine(Math.Abs(x - 0.3) < 1e-9);
Console.WriteLine(x.ToString("F2"));
$ dotnet run tolerancia.cs
True
0,30
Math.Abs(x - 0.3) < 1e-9
Math.Abs é o valor absoluto (tira o sinal). A pergunta é: "a distância entre x e 0,3 é menor que um bilionésimo?". Sim: True.
x.ToString("F2")
Para mostrar, arredonde na hora de escrever: "F2" = 2 casas fixas, 0,30. O valor guardado continua o mesmo.

Qual usar?

SituaçãoTipo
Preço, saldo, salário, troco, juros, impostosdecimal (19.90m)
Altura, peso, temperatura, distância, média de notasdouble (1.65)
Contas científicas, Math.Sqrt, Math.Sin (elas recebem e devolvem double)double
Jogos e gráficos (Unity usa float), milhões de valores na memóriafloat (1.5f)
Contagens, sem vírgulanenhum deles: int

Quando dá errado

error CS0664: Literal do tipo double não pode ser convertido implicitamente no tipo "decimal"; use um sufixo "M"…

O que significa: um número com ponto e sem sufixo é double, e foi para uma caixa decimal (ou float: aí a mensagem pede o sufixo "F"). Em inglês: Literal of type double cannot be implicitly converted to type 'decimal'; use an 'M' suffix to create a literal of this type.

Como consertar: o sufixo: 19.90m, 62.5f.

error CS0019: O operador "*" não pode ser aplicado a operandos dos tipos "decimal" e "double"

O que significa: uma conta mistura decimal com double. Quase sempre é um número escrito sem m.

Como consertar: ponha m no número (1.1m) ou converta a variável com (decimal).

Saiu 0,30000000000000004 ou 0,9999999999999999

O que significa: o double guardou o valor mais próximo, não o exato, e o .NET mostra todos os dígitos.

Como consertar: se é dinheiro, troque para decimal. Se é medida, só arredonde na hora de mostrar (ToString("F2")) e compare com tolerância.

Um if (x == 0.3) nunca é verdadeiro

O que significa: o valor calculado é vizinho de 0,3, mas não igual.

Como consertar: Math.Abs(x - 0.3) < 1e-9, ou use decimal.

Saiu ∞ ou NaN

O que significa: divisão por zero com double (∞, -∞) ou uma conta sem resultado, como 0.0 / 0 (NaN).

Como consertar: confira o divisor antes de dividir. Para testar o resultado, double.IsInfinity e double.IsNaN, nunca == double.NaN.

System.DivideByZeroException: Attempted to divide by zero.

O que significa: divisão por zero com decimal ou int. O programa parou (código de saída 134).

Como consertar: confira antes: if (pessoas != 0) (aula if e else).


Referência rápida

EscritaO que é
1.5, 1.5ddouble (o padrão para números com ponto)
1.5ffloat
1.5mdecimal
1.5e3, 2.5e-4Notação científica: 1500 e 0,00025
double.MaxValue, decimal.MaxValue, float.MaxValueOs maiores valores
double.PositiveInfinity, double.NaNOs valores especiais (∞ e NaN)
double.IsNaN(x), double.IsInfinity(x)Testes de NaN e infinito
Math.Abs(a - b) < 1e-9Comparar double com tolerância
x.ToString("F2"), valor.ToString("C")Mostrar com 2 casas; mostrar como moeda (R$ 59,70)
Math.Round(valor, 2)Arredondar para 2 casas

Exercícios

1. O sufixo do dinheiro

Complete para o código compilar:

decimal salario = 2500.50;

2. O sufixo do float

Complete para o código compilar:

float nota = 7.5;

3. Notação científica

Quanto vale 2e3? (Escreva só o número.)

4. O que Console.WriteLine(0.1 + 0.2); mostra no Ubuntu em português?

5. Qual tipo usar para o saldo de uma conta bancária?

6. O que acontece em double r = 5.0 / 0;?

7. decimal total = preco * 1.05; (com preco decimal) não compila. Por quê?

Missão final: o caixa das balas

Missão: o caixa da lojinha soma 10 balas de R$ 0,10 e mostra Total: 0,9999999999999999. Troque o tipo de total e o do preço da bala para o tipo de dinheiro (não esqueça o sufixo), para mostrar Total: 1,00.

double total = 0;
for (int i = 0; i < 10; i++)
{
    total = total + 0.10;
}
Console.WriteLine("Total: " + total);
decimal total = 0;
for (int i = 0; i < 10; i++)
{
    total = total + 0.10m;
}
Console.WriteLine("Total: " + total);

Se você trocar só o tipo de total e esquecer o m do 0.10, o compilador reclama com o CS0019 desta aula: decimal mais double.


Resumo

  • double é o padrão para números com ponto (1.65): 8 bytes, uns 15 a 17 dígitos. float (1.5f): 4 bytes, uns 7 dígitos. decimal (19.90m): 16 bytes, 28 a 29 dígitos, exato em base 10.
  • 0.1 + 0.2 dá 0,30000000000000004 com double e 0,3 com decimal. Dez vezes 0.10 dá 0,9999999999999999 e 1,00.
  • Dinheiro é sempre decimal. Medidas, double.
  • Sem sufixo, o número com ponto é double: CS0664 numa caixa float ou decimal. decimal e double não se misturam: CS0019.
  • Nunca compare double com ==: use tolerância (Math.Abs(a - b) < 1e-9).
  • Divisão por zero: double dá ∞, -∞ ou NaN e segue; decimal e int lançam DivideByZeroException.