Um decimal em C# armazena um número como um inteiro de 96 bits mais uma escala que indica onde fica o ponto decimal. É por isso que ele conta em décimos e centésimos como nós, e é por isso que 0.1m + 0.2m é exatamente 0.3m, enquanto a mesma soma em double não é.
A exatidão tem um preço: o intervalo de valores. Um decimal vai até 79.228.162.514.264.337.593.543.950.335 e não passa disso, o que dá 28 ou 29 dígitos significativos, dependendo do número. Todo o restante do artigo trata de gastar esses dígitos de forma deliberada, seja no que exibimos, seja no que armazenamos.
O que é o tipo decimal em C#?
O tipo decimal em C# é um número de 128 bits formado por um bit de sinal, um inteiro de 96 bits e uma escala que registra quantas casas o ponto decimal deve ser deslocado para a esquerda. Um literal decimal leva o sufixo m ou M, porque um literal sem sufixo, como 1.5, é um double.
Como a escala conta potências de dez, e não de dois, o decimal representa os valores que as pessoas anotam. 0.1m é exatamente um décimo. Em double, 0.1 é a fração binária mais próxima de um décimo, e é por isso que 0.1 + 0.2 produz 0.30000000000000004 e 0.1m + 0.2m produz 0.3.
É essa exatidão que faz dele o tipo certo para dinheiro e para qualquer coisa que uma pessoa vá auditar. O custo está no intervalo e no armazenamento: um decimal chega no máximo a 79.228.162.514.264.337.593.543.950.335 e ocupa 16 bytes de memória, enquanto um double ocupa apenas 8.
Primeiro, decimal é um valor de ponto flutuante de 128 bits na base 10 (embora apenas 102 bits sejam de fato usados). Ele é composto por 96 bits para um número inteiro, 5 bits para a escala e um bit de sinal. A principal vantagem do decimal em cálculos financeiros ou em certos cálculos científicos é o fato de ser um valor de ponto flutuante na base 10, e não binário. Isso ajuda a evitar muitos dos erros de arredondamento que vemos em tipos como float e double. Para se aprofundar nos problemas de arredondamento, confira o nosso artigo sobre igualdade de números de ponto flutuante.
Vamos ilustrar isso com um exemplo numérico simples:
decimal highPrecisionValue = 123456789.1234567890123456789012345M;
double regularDouble = 123456789.1234567890123456789012345;
float regularFloat = 123456789.1234567890123456789012345f;
Console.WriteLine($"Decimal: {highPrecisionValue}");
Console.WriteLine($"Double: {regularDouble}");
Console.WriteLine($"Float: {regularFloat}");
Aqui, definimos três valores “idênticos”, mas os atribuímos a três tipos de ponto flutuante diferentes do C#: decimal, double e float, respectivamente. Observe também como usamos M para definir uma constante decimal e f para indicar uma constante float. Isso porque, por padrão, em C# uma constante de ponto flutuante é considerada um double.
Agora, vamos analisar a saída:
Decimal: 123456789.12345678901234567890 Double: 123456789.12345679 Float: 123456790
Aqui, vemos claramente a diferença de precisão disponível entre decimal, double e float. Esse exemplo mostra por que o decimal é o tipo preferido para cálculos financeiros e outros cálculos que exigem muita precisão. Para ter o quadro completo de como decimal se compara a double e float, incluindo quando cada um é a escolha certa, temos um artigo dedicado.
Com isso em mente, vamos ver como controlar a precisão desses valores.
Por que um decimal em C# tem 28 ou 29 dígitos significativos?
O inteiro de 96 bits dentro de um decimal pode conter qualquer valor até 79.228.162.514.264.337.593.543.950.335. Esse número tem 29 dígitos, então 29 dígitos é o teto. Mas nem todo número de 29 dígitos cabe: qualquer valor acima desse limite precisa de um 97º bit e, por isso, é arredondado ou causa estouro. Todo número de 28 dígitos cabe, sem exceção.
Isso é tudo o que há por trás de “28 ou 29, dependendo do valor”. Vinte e oito dígitos estão disponíveis a partir de 0,1, e o vigésimo nono só a partir de 1, enquanto o número fica abaixo do limite.
A escala é um orçamento à parte. Ela vai de 0 a 28 e decide quantos desses dígitos ficam depois do ponto. 1.10m e 1.1m são iguais na comparação e representam a mesma quantidade, mas têm escalas diferentes e são impressos de forma diferente, porque um decimal guarda o zero à direita que recebeu.
Atribua um literal mais longo, e o compilador simplesmente o arredonda para caber.
A divisão mostra o limite diretamente: 907m / 31m dá 29.258064516129032258064516129, exatamente 29 dígitos significativos, porque o quociente nunca termina e o tipo preenche todos os dígitos que tem.
| Propriedade | Valor |
|---|---|
| Tipo .NET | System.Decimal |
| Tamanho | 128 bits, dos quais 102 contêm informação |
| Composição | bit de sinal + inteiro de 96 bits + escala (0 a 28) |
| Dígitos significativos | 28 ou 29, dependendo do valor |
decimal.MaxValue | 79228162514264337593543950335 |
decimal.MinValue | -79228162514264337593543950335 |
| Menor valor diferente de zero | 0.0000000000000000000000000001 (1 x 10^-28) |
| Sufixo de literal | m ou M, como em 123.45m |
| Valor padrão | 0m |
| Arredondamento padrão | MidpointRounding.ToEven (arredondamento bancário); as strings de formato arredondam os empates para longe do zero |
Como controlar a precisão exibida de um decimal?
Exibir um decimal com menos dígitos não altera o valor. ToString() gera uma nova string; o decimal na variável permanece intacto, e o próximo cálculo continua usando todos os dígitos que ele tinha.
Há duas formas de dizer quantos dígitos queremos. Uma string de formato personalizada descreve o formato diretamente: myDecimal.ToString("0.00") dá duas casas, "0.0000" dá quatro. Uma string de formato padrão nomeia um estilo, e "F" obtém a quantidade de dígitos do próprio especificador de precisão ou de um NumberFormatInfo: tanto "F3" quanto ToString("F", format) com NumberDecimalDigits definido como 3 dão três casas.
A string personalizada é a escolha certa por padrão. Ela não exige nenhum objeto extra e o resultado fica visível na própria string. O caminho com NumberFormatInfo compensa quando a quantidade de dígitos é uma variável, e não uma constante, ou quando as mesmas configurações são reutilizadas em muitos valores.
As duas arredondam, em vez de truncar.
Para se aprofundar nas strings de formato disponíveis, confira o nosso artigo ‘Strings de formato numérico padrão e personalizadas em C#‘.
Controle da precisão do decimal com strings de formato personalizadas
Primeiro, vamos ver como usar strings de formato personalizadas para controlar a precisão de um decimal. Vamos testar isso restringindo a parte fracionária do valor a duas casas decimais com a string de formato personalizada "0.00":
const decimal myDecimal = 123.456789M;
Console.WriteLine($"Value (\"0.00\"): {myDecimal.ToString("0.00")}");
Console.WriteLine($"Value (default format): {myDecimal}");
Aqui, definimos um valor decimal myDecimal com mais de 2 casas decimais. Em seguida, chamamos ToString() com a string de formato personalizada "0.00" e imprimimos o valor resultante no console. Depois, imprimimos o valor original sem nenhuma formatação personalizada.
Ao examinar a saída, vemos que o valor interno permaneceu inalterado (como mostra a segunda chamada a WriteLine()), mas, quando o imprimimos com a string de formato personalizada, a quantidade de casas decimais exibidas fica restrita:
Value ("0.00"): 123.46
Value (default format): 123.456789
Para exibir mais dígitos, basta ajustar a string de formato. Por exemplo, para exibir 4 casas decimais:

E-book gratuito
Sua Web API está pronta para produção?
33 itens para verificar antes de implantá-la, com a correção de cada um. Um PDF gratuito de 76 páginas para .NET 10.
O e-book está em inglês.
Baixe o checklist gratuitoPDF gratuito. Um único e-mail para enviá-lo. Cancele a inscrição quando quiser.
Console.WriteLine($"Value (\"0.0000\"): {myDecimal.ToString("0.0000")}");
O resultado é:
Value ("0.0000"): 123.4568
Uso do NumberFormatInfo
Outra opção para formatar a saída de valores decimal é usar a classe NumberFormatInfo. Embora essa classe tenha uma infinidade de opções para controlar a saída do valor, aqui vamos nos concentrar apenas em controlar a quantidade de dígitos exibidos. Fazemos isso definindo a propriedade NumberDecimalDigits em um método de extensão. Daqui em diante, todos os métodos de extensão ficam dentro de uma classe estática, que declaramos em um novo arquivo, DecimalExtensions.cs:
using System.Globalization;
public static string ToStringXDecimalPlaces(this decimal val, int decimalPlaces)
{
var format = new NumberFormatInfo
{
NumberDecimalDigits = decimalPlaces
};
return val.ToString("F", format);
}
Primeiro, inicializamos uma nova instância de NumberFormatInfo e definimos NumberDecimalDigits de acordo com a precisão especificada. Em seguida, retornamos o decimal como uma string formatada com o formato numérico padrão de ponto fixo e o objeto NumberFormatInfo.
A propriedade NumberDecimalDigits só se aplica quando usamos as strings de formato numérico padrão “N” (número) ou “F” (ponto fixo). Para mais informações sobre essas e outras strings de formato, podemos consultar a documentação do .NET sobre strings de formato numérico padrão.
Vamos ver o método de extensão em ação:
Console.WriteLine($"Value (NumberFormatInfo 3 digits): {myDecimal.ToStringXDecimalPlaces(3)}");
O resultado é:
Value (NumberFormatInfo 3 digits): 123.457
Com strings de formato, podemos imprimir números decimais sem alterar o valor numérico deles. No entanto, também podemos usar o arredondamento para controlar a precisão interna.
Como controlar a precisão armazenada de um decimal?
Alterar o valor armazenado significa chamar um de quatro métodos, e cada um retorna um novo decimal em vez de alterar o que passamos.
decimal.Round() é o que recebe uma quantidade de dígitos: decimal.Round(value, 2) mantém duas casas. Por padrão, ele resolve os empates arredondando para o dígito par mais próximo, então 2.5m é arredondado para 2 e 3.5m é arredondado para 4. Passar MidpointRounding.AwayFromZero dá o comportamento que a maioria das pessoas aprendeu na escola, em que 2.5m vira 3.
Math.Round() produz o mesmo resultado para um argumento decimal, então a escolha entre os dois é questão de estilo, e não de comportamento.
Os outros três não recebem quantidade de dígitos e sempre chegam a um número inteiro. Truncate() descarta a parte fracionária e vai em direção a zero. Floor() vai em direção ao infinito negativo. Ceiling() vai em direção ao infinito positivo.
Em números positivos, Truncate() e Floor() parecem idênticos. Em -15.6789m, eles divergem, e Truncate() passa a coincidir com Ceiling(): Truncate() dá -15, Floor() dá -16 e Ceiling() dá -15. Essa divergência é a única coisa que vale memorizar sobre eles.
Controle da precisão do decimal com arredondamento
As funções de arredondamento são essenciais para controlar a precisão de números decimal, pois nos permitem escolher quantos dígitos de precisão o valor mantém.
O arredondamento continua necessário mesmo com um tipo exato na base 10. A referência da Microsoft para a estrutura System.Decimal diz isso com todas as letras: “O tipo Decimal não elimina a necessidade de arredondamento.”
Em C#, a função Math.Round(), um dos métodos de arredondamento da classe Math, arredonda um valor para o inteiro mais próximo ou para um número específico de casas decimais. Também podemos usar a função equivalente decimal.Round(), um método estático que o System.Decimal oferece há muito tempo e que tem o mesmo comportamento. Por padrão, o método Round() usa a estratégia MidpointRounding.ToEven:
public static decimal Round(this decimal value, int decimalPlaces)
=> decimal.Round(value, decimalPlaces);
Aqui, criamos um método de extensão simples que chama decimal.Round() para arredondar o valor para o número de casas especificado.

E-book gratuito
Sua Web API está pronta para produção?
33 itens para verificar antes de implantá-la, com a correção de cada um. Um PDF gratuito de 76 páginas para .NET 10.
O e-book está em inglês.
Baixe o checklist gratuitoPDF gratuito. Um único e-mail para enviá-lo. Cancele a inscrição quando quiser.
Agora, vamos vê-lo em ação:
Console.WriteLine($"Value (default format): {myDecimal}");
Console.WriteLine($"Value (round 2 places): {myDecimal.Round(2)}");
Observe que estamos imprimindo os dois valores com o formato padrão do decimal:
Value (default format): 123.456789 Value (round 2 places): 123.46
O método Truncate
Também podemos usar o método Truncate() para controlar a precisão do valor. Truncate() remove a parte fracionária e deixa apenas a parte inteira do valor. Vamos criar um método de extensão para testá-lo:
public static decimal Truncate(this decimal value) => decimal.Truncate(value);
Ao chamá-lo com o nosso valor decimal (123.456789):
Console.WriteLine($"Value (truncate): {myDecimal.Truncate()}");
Obtemos:
Value (truncate): 123
Os métodos Ceiling e Floor
Ceiling() e Floor() são parecidos com Truncate(), pois os dois retornam um valor sem a parte fracionária. A diferença é que, enquanto Truncate() simplesmente descarta a parte fracionária, Ceiling() e Floor() se comportam mais como Round(). Ceiling() retorna o menor inteiro maior ou igual ao valor, e Floor() retorna o maior inteiro menor ou igual a ele. Quando o objetivo é um número inteiro, e não uma quantidade fixa de casas, o nosso artigo sobre como arredondar um número para baixo até o inteiro mais próximo trata desse caso separadamente.
Vamos criar mais alguns métodos de extensão para testar essas operações:
public static decimal Ceiling(this decimal value) => decimal.Ceiling(value); public static decimal Floor(this decimal value) => decimal.Floor(value);
Esses métodos não passam de um pouco de açúcar sintático que nos permite chamá-los diretamente no valor decimal do exemplo:
Console.WriteLine($"Value (ceiling): {myDecimal.Ceiling()}");
Console.WriteLine($"Value (floor): {myDecimal.Floor()}");
O resultado é:
Value (ceiling): 124 Value (floor): 123
| Método | 12.3456 | 7.0 | -15.6789 | O que faz |
|---|---|---|---|---|
decimal.Round(v, 2) | 12.35 | 7.0 | -15.68 | Arredonda para 2 casas decimais, com desempate para o número par |
decimal.Truncate(v) | 12 | 7 | -15 | Descarta a parte fracionária, em direção a zero |
decimal.Floor(v) | 12 | 7 | -16 | Maior inteiro menor ou igual ao valor |
decimal.Ceiling(v) | 13 | 7 | -15 | Menor inteiro maior ou igual ao valor |
As colunas 12.3456 e -15.6789 trazem os valores que os testes do projeto de exemplo verificam, então a tabela e o repositório coincidem.
O que acontece ao converter float ou double em decimal?
Uma conversão para decimal não recupera a precisão que nunca existiu, nem preserva toda a que existia. Ela arredonda, e o quanto mantém depende do tipo de origem.
Converter a partir de float mantém 7 dígitos significativos. A partir de double, mantém 15. É por isso que (decimal)(float)2222.998 dá 2222.998, embora o float contenha, na verdade, 2222.998046875: sete dígitos dão 2222.998, e o resto é descartado. Passe o mesmo valor por double, com (decimal)(double)(float)2222.998, e 2222.998046875 sobrevive por inteiro.
A lição não é sobre conversão. É que um valor que passa por um float já perdeu exatidão, e convertê-lo em decimal depois só decide quanto do estrago fica visível.
Por isso, um valor que precisa ser exato é declarado como decimal no ponto em que entra no programa, a partir de um literal ou de uma string convertida, e nunca passa por float ou double no caminho.
Quando essa string chega em tempo de execução, decimal.TryParse() e os métodos de conversão são a forma de transformá-la em um decimal sem desviar por um tipo binário.
Conclusão
Neste artigo, vimos várias técnicas para controlar a precisão de valores decimal. Primeiro, examinamos como controlar o formato de saída sem modificar o armazenamento interno dos valores. Depois, examinamos técnicas para controlar a precisão interna deles. No fim das contas, a opção que escolhemos depende do caso de uso. Se quisermos manter um alto nível de precisão, provavelmente devemos nos concentrar apenas em ajustar a formatação de exibição dos dados. Por outro lado, quando temos dados menos precisos, talvez queiramos usar uma das técnicas de arredondamento para reduzir a precisão interna dos valores. Para mais informações sobre o tipo decimal, confira o excelente artigo de Jon Skeet sobre o assunto.
Testado com .NET 10.