Parsing, TryParse e o Padrão Try
Objective
Texto é de onde vêm números, datas e enums: formulários, arquivos, query
strings, headers, configuração. Transformar esse texto em valores tipados é o
lugar mais comum em que uma exceção vira evento normal, e o lugar mais comum em
que um bug de cultura vira corrupção silenciosa de dados. Este conceito cobre
as duas famílias de métodos de parsing (Parse e TryParse), o que os
NumberStyles padrão aceitam, as regras de cultura do lado da leitura, o
parsing exato de datas, o Enum.TryParse e suas armadilhas, as interfaces
IParsable<T> e ISpanParsable<T> que unificam tudo isso, e como escrever seu
próprio método Try com a semântica certa. O lado da formatação do problema de
cultura está em formatting-values-and-interpolated-strings; este conceito
acrescenta o lado da leitura com saída medida.
Use Cases
- Ler um campo digitado ou um campo de CSV para um número sem um try/catch em volta de cada linha.
- Interpretar datas que chegam como
dd/MM/yyyy, ISO 8601 com offset ou vários formatos aceitos. - Ligar um argumento de linha de comando, uma variável de ambiente ou um valor de query string a um enum.
- Escrever um método genérico (
T Read<T>(string s) where T : IParsable<T>) e um tipo de valor que faz parsing como os tipos internos. - Escolher entre uma API baseada em exceção e uma API
TryXao desenhar uma biblioteca.
Deep Dive
A saída é do .NET 10 (SDK 10.0.100) em arm64. Salvo quando uma cultura é
nomeada, os exemplos colocam CultureInfo.CurrentCulture na cultura invariante,
para que a saída não dependa da máquina.
Parse lança, TryParse responde
Parse devolve o valor ou lança exceção. TryParse devolve um bool e coloca
o valor em um parâmetro out, então não há exceção para entrada ruim comum:
csharpint.Parse("42") // 42
int.Parse("4x") // FormatException
int.Parse("") // FormatException
int.Parse(null) // ArgumentNullException
int.Parse("2147483648") // OverflowException
if (int.TryParse(text, out var n)) { /* n é válido */ }
int.TryParse((string?)null, out var a) // False, a = 0
int.TryParse("4x", out var b) // False, b = 0
int.TryParse("2147483648", out var c) // False, c = 0 (overflow é só false)A declaração out var (C# 7) mantém o valor no escopo depois do if. Na
falha o valor de saída é default (a documentação diz zero), e ele é
sobrescrito mesmo que tivesse algo antes, então nunca leia sem checar o
resultado.
Uma exceção continua existindo no TryParse, porque o "Try" cobre só a falha
bem definida do texto. Uma combinação inválida de NumberStyles é erro de
programação, e continua lançando ArgumentException:
csharpint.TryParse("1", NumberStyles.HexNumber | NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out _);
// ArgumentException: With the AllowHexSpecifier or AllowBinarySpecifier bit set ...
int.TryParse("1", (NumberStyles)0x10000000, CultureInfo.InvariantCulture, out _);
// ArgumentException: An undefined NumberStyles value is being used.O que o texto pode conter: NumberStyles
Cada tipo numérico tem um estilo padrão, e ele é mais estrito para inteiros do
que para ponto flutuante. O padrão de inteiros (NumberStyles.Integer) permite
espaço em branco no início e no fim e um sinal no início. O padrão de ponto
flutuante (Float | AllowThousands) também permite ponto decimal, expoente e
separadores de grupo:
csharp// int.TryParse (estilo padrão)
" 42 " -> 42 "+42" -> 42 "-42" -> -42
"(42)" "1,234" "1_000" "1e3" "42.0" "0x2A" -> false
// double.TryParse(s, InvariantCulture, out ...)
"1.5" -> 1.5 "1,234.5" -> 1234.5 "1e3" -> 1000 "1E-2" -> 0.01
".5" -> 0.5 "5." -> 5 " 3.14 " -> 3.14
"NaN" -> NaN "Infinity" -> Infinity "-Infinity" -> -Infinity
"1e400" -> Infinity (true, e o valor é infinito)
"1,5" -> 15 (a vírgula é separador de grupo, sem erro)
"0x10" "1 234.5" "∞" -> falseA linha do 1e400 importa: um texto que estoura o double ainda devolve true
com Infinity. int, decimal e os outros tipos inteiros devolvem false
em overflow. Valide com double.IsFinite depois de fazer parsing de entrada
externa.
Você escolhe o estilo explicitamente quando o padrão está errado:
csharpint.TryParse("1,234", NumberStyles.Integer | NumberStyles.AllowThousands, inv, out v) // True 1234
int.TryParse("FF", NumberStyles.HexNumber, inv, out v) // True 255
int.TryParse("0xFF", NumberStyles.HexNumber, inv, out v) // False (prefixo não é permitido)
int.TryParse("FFFFFFFF", NumberStyles.HexNumber, inv, out v) // True -1 (complemento de dois)
decimal.TryParse("$1,234.50", NumberStyles.Currency, en-US, out d) // True 1234.50
decimal.TryParse("1.234,50 €", NumberStyles.Currency, de-DE, out d) // True 1234.50
int.TryParse(" 42", NumberStyles.None, inv, out v) // False (só dígitos)
uint.TryParse("-0", out u) // True 0 uint.TryParse("-1", out u) // Falsedecimal.Parse mantém a escala do texto ("1.10" dá 1.10, "1.100" tem
Scale 3) e NumberStyles.Float aceita "1e2" como 100. Um valor fora da
faixa do decimal, como "1e29", é false.
A cultura do lado da leitura
Parse e TryParse sem provider usam CultureInfo.CurrentCulture. O mesmo
texto dá números diferentes em lugares diferentes, e muitas vezes não dá erro.
Saída de double.TryParse(text, Float | AllowThousands, culture, ...):
csharpcultura texto valor
invariant "1,234.56" 1234.56
en-US "1,234.56" 1234.56
pt-BR "1.234,56" 1234.56
de-DE "1.234,56" 1234.56
invariant "1.234,56" false
pt-BR "1,234.56" false
pt-BR "1.5" 15 <- o ponto é separador de grupo, em silêncio
invariant "1,5" 15 <- a vírgula é separador de grupo, em silêncio
pt-BR "1,5" 1.5
pt-BR "1.234" 1234 <- mil, não 1.234
invariant "1.234" 1.234As linhas perigosas são as sem erro. Um valor escrito por uma máquina pt-BR e lido por um parser invariante fica errado por um fator de 10:
csharpCultureInfo.CurrentCulture = new CultureInfo("pt-BR");
string written = 1234.5.ToString(); // "1234,5"
double.Parse(written, CultureInfo.InvariantCulture) // 12345
double.Parse(written, new CultureInfo("pt-BR")) // 1234.5
1234.5.ToString(CultureInfo.InvariantCulture) // "1234.5" (seguro para guardar)A regra é simétrica à da formatação: dado que um programa guarda ou troca usa
CultureInfo.InvariantCulture nas duas pontas, e o texto que uma pessoa digita
é lido com a cultura dessa pessoa. Separadores de grupo também são dado de
cultura: de-CH usa ' e fr-FR usa U+202F (espaço estreito sem quebra), e
nesta execução "1 234,5" com espaço normal e com U+202F foram lidos como
1234.5 em fr-FR, então testes com texto copiado de uma planilha merecem um
caso real.
Datas e horas
DateTime.TryParse depende da cultura tanto quanto os números, e uma data tem
duas interpretações que parecem ambas válidas:
csharpcultura texto data
invariant "03/04/2026" 2026-03-04
en-US "03/04/2026" 2026-03-04
pt-BR "03/04/2026" 2026-04-03
de-DE "03.04.2026" 2026-04-03
invariant "2026-04-03" 2026-04-03 (a ordem ISO não é ambígua)
en-US "13/04/2026" false
invariant "2026-02-30" falseQuando o formato faz parte de um contrato, use TryParseExact com o formato e
InvariantCulture. Ele é estrito quanto à forma:
csharpDateTime.TryParseExact("03/04/2026", "dd/MM/yyyy", inv, DateTimeStyles.None, out d) // 2026-04-03
DateTime.TryParseExact("03/04/2026", "MM/dd/yyyy", inv, DateTimeStyles.None, out d) // 2026-03-04
DateTime.TryParseExact("3/4/2026", "dd/MM/yyyy", inv, DateTimeStyles.None, out d) // False
DateTime.TryParseExact("3/4/2026", "d/M/yyyy", inv, DateTimeStyles.None, out d) // True
DateTime.TryParseExact("03/04/2026 ", "dd/MM/yyyy", inv, DateTimeStyles.AllowWhiteSpaces, out d) // True
DateTime.TryParseExact("2026-04-03", new[] { "yyyy-MM-dd", "dd/MM/yyyy" }, inv, DateTimeStyles.None, out d) // TrueDateOnly.TryParseExact e TimeOnly.TryParse existem para valores que não são
instantes. Para um instante com offset, prefira DateTimeOffset, porque o
DateTime perde o offset: DateTime.Parse("2026-10-15T09:05:07Z") devolveu um
DateTime com Kind=Local, já convertido para o fuso da máquina
(10:05:07+01:00 aqui), enquanto DateTimeStyles.RoundtripKind manteve
Kind=Utc e 09:05:07Z. Uma string sem fuso dá Kind=Unspecified.
DateTimeOffset.TryParse("2026-10-15T09:05:07+02:00") mantém o +02:00.
TimeSpan.TryParse("90") é 90.00:00:00, noventa dias, porque um número solto
é dias. Para horas de relógio use TryParseExact com um formato como
"hh\\:mm".
Enum.TryParse e suas armadilhas
csharpenum Color { Red, Green, Blue }
Enum.TryParse<Color>("Green", out c) // True Green
Enum.TryParse<Color>("green", out c) // False (diferencia maiúsculas por padrão)
Enum.TryParse<Color>("green", true, out c) // True Green
Enum.TryParse<Color>(" Green ", out c) // True Green (espaços são removidos)
Enum.TryParse<Color>("1", out c) // True Green (um número é aceito)
Enum.TryParse<Color>("99", out c) // True 99 (não é um membro definido)
Enum.IsDefined(c) // False
Enum.TryParse<Color>("-1", out c) // True -1
Enum.TryParse<Color>("Red, Blue", out c) // True Blue (combinado, mesmo sem [Flags])
Enum.Parse<Color>("Purple") // ArgumentExceptionUm enum é um inteiro com nomes, então o parser aceita qualquer texto de
inteiro, e true não significa que o valor é um membro declarado. Quando o
texto vem de fora, siga o parse com Enum.IsDefined, ou rejeite textos que
começam com dígito ou sinal de menos. Em enums [Flags] o separador é a
vírgula: "Read, Write" dá Read, Write (3) e "Read|Write" é false.
IParsable<T> e ISpanParsable<T>
O .NET 7 transformou a convenção em interfaces. IParsable<TSelf> declara dois
membros estáticos, e ISpanParsable<TSelf> acrescenta os overloads com
ReadOnlySpan<char>:
csharppublic interface IParsable<TSelf> where TSelf : IParsable<TSelf>
{
static abstract TSelf Parse(string s, IFormatProvider? provider);
static abstract bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, out TSelf result);
}int, double, decimal, DateTime, DateOnly, TimeSpan, Guid,
IPAddress e os outros tipos numéricos a implementam (a página do Microsoft
Learn lista todos), e os tipos numéricos também implementam
IUtf8SpanParsable<T>, então int.TryParse("12345"u8, out v) faz parsing
direto de bytes UTF-8. Como os membros são static abstract, um método
genérico faz parsing sem reflection:
csharpstatic IEnumerable<T> ParseAll<T>(string text, IFormatProvider? p) where T : IParsable<T>
{
foreach (var part in text.Split(' ', StringSplitOptions.RemoveEmptyEntries))
yield return T.Parse(part, p);
}
ParseAll<int>("1 2 3", inv) // 1,2,3
ParseAll<double>("1.5 2.5", inv) // 1.5,2.5
ParseAll<DateOnly>("2026-10-15 2026-10-16", inv) // 10/15/2026,10/16/2026 (impresso pelo DateOnly.ToString)
static T TryParseOrDefault<T>(string s, T fallback, IFormatProvider? p) where T : IParsable<T>
=> T.TryParse(s, p, out var r) ? r : fallback;Seu próprio tipo entra na família implementando a interface. Os overloads de span guardam a lógica, e os de string delegam para eles:
csharpreadonly record struct Point(double X, double Y) : ISpanParsable<Point>
{
public static Point Parse(string s, IFormatProvider? provider)
{
ArgumentNullException.ThrowIfNull(s);
return Parse(s.AsSpan(), provider);
}
public static Point Parse(ReadOnlySpan<char> s, IFormatProvider? provider)
=> TryParse(s, provider, out var r) ? r : throw new FormatException($"'{s}' is not a Point (expected X;Y).");
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, out Point result)
=> TryParse(s.AsSpan(), provider, out result);
public static bool TryParse(ReadOnlySpan<char> s, IFormatProvider? provider, out Point result)
{
result = default;
int semi = s.IndexOf(';');
if (semi < 0) return false;
if (!double.TryParse(s[..semi], NumberStyles.Float, provider, out var x)) return false;
if (!double.TryParse(s[(semi + 1)..], NumberStyles.Float, provider, out var y)) return false;
result = new Point(x, y);
return true;
}
}
Point.Parse("3.5;-2", inv) // Point { X = 3.5, Y = -2 }
Point.Parse("3,5;-2", pt-BR) // Point { X = 3.5, Y = -2 }
Point.TryParse("3.5", inv, out p) // False
Point.TryParse((string?)null, inv, out p) // False
Point.Parse("a;b", inv) // FormatException: 'a;b' is not a Point (expected X;Y).
Point.Parse(null!, inv) // ArgumentNullExceptionO provider é repassado às chamadas internas, então o tipo não decide a cultura
por conta própria. Fazer parsing a partir de um span também evita alocações.
Este laço soma os campos válidos de uma linha sem criar nenhuma substring:
csharpReadOnlySpan<char> line = "10,20,x,40";
int total = 0, bad = 0;
foreach (var range in line.Split(','))
if (int.TryParse(line[range], inv, out var v)) total += v; else bad++;
// total 70, bad 1Escrevendo seu próprio método Try
As Framework Design Guidelines (a página do Microsoft Learn é uma reimpressão
da edição de 2008 e avisa que partes podem estar desatualizadas, mas o padrão é
o que a BCL ainda segue) definem a forma: o nome começa com Try, o tipo de
retorno é bool, o resultado vai em um parâmetro out, e você também fornece
a contraparte que lança exceção. O Try cobre uma falha bem definida; qualquer
outra coisa (um argumento nulo que é bug, uma opção inválida) continua
lançando.
csharpreadonly record struct Percent(int Value)
{
public static bool TryParse(string? s, out Percent result)
{
result = default;
if (s is null || !s.EndsWith('%')) return false;
if (!int.TryParse(s.AsSpan(0, s.Length - 1), NumberStyles.None, CultureInfo.InvariantCulture, out var v) || v > 100)
return false;
result = new Percent(v);
return true;
}
}
// "45%" -> True 45 "145%" -> False "abc" -> False
record Customer(int Id, string Name)
{
public static bool TryFind(int id, [NotNullWhen(true)] out Customer? customer)
=> Db.TryGetValue(id, out customer);
}Atribua result primeiro (o compilador obriga) e use [NotNullWhen(true)] em
um parâmetro out de tipo por referência para que os chamadores tenham análise
de nulidade: depois de if (TryFind(1, out var c)), c é conhecido como não
nulo. A mesma forma está por toda a BCL: Dictionary.TryGetValue,
Queue.TryDequeue, Guid.TryParse, Uri.TryCreate. A alternativa mais antiga
é o padrão tester-doer (if (dict.ContainsKey(k)) use(dict[k])), que faz duas
buscas e tem uma janela de corrida em código concorrente; o método Try faz o
teste e o trabalho em uma chamada.
O custo da exceção, medido
Build Release, sem debugger anexado, 200.000 parses de int em que 6 de 8
entradas são inválidas:
csharpParse + catch (FormatException) 427 ms
TryParse 1 ms
um Parse + catch com falha 2.7 us
um TryParse com falha 5.5 ns
um Parse com sucesso 5.0 nsUm throw custa aqui cerca de 500 vezes mais que um TryParse que falha, e um
Parse com sucesso é tão barato quanto um TryParse. Por isso a guideline diz
que um método Try serve para o caso que falha na operação normal.
Trade-offs
TryParsepara entrada não confiável,Parsepara entrada confiável. UmParseem uma constante, em um valor de configuração que você escreveu ou em um arquivo que você gerou dá uma exceção clara com stack trace quando a premissa quebra. UseTryParseonde texto inválido é um caso normal e reporte um bom erro ao usuário.TryParseesconde por que falhou. Ofalsenão diz se o texto estava vazio, malformado ou grande demais. Se o chamador precisa do motivo (um formulário com mensagens por campo), faça o parsing em etapas ou devolva um tipo de resultado com um código de erro, um custo de design que a assinatura comboolnão carrega.- Os estilos padrão são permissivos de formas que você pode não querer.
doubleaceita separadores de grupo eInfinity;Enumaceita qualquer número. PasseNumberStyles.NoneouNumberStyles.AllowLeadingSignpara identificadores e contagens, e chequedouble.IsFiniteeEnum.IsDefineddepois. - Um valor errado em silêncio é pior que uma exceção.
"1.5"lido como15e03/04/2026lido como março ou abril não falham. Qualquer parser que lê dados guardados ou trocados precisa nomear a cultura e, para datas, o formato exato. IParsable<T>compra código genérico e um contrato estável, ao preço de implementar quatro métodos. Para um tipo usado por um único chamador, um único métodoParsebasta. Implemente a interface quando o tipo for um value object que o código genérico (binding, serializadores, leitores genéricos) deva tratar.- Implementar
ISpanParsable<T>empurra o trabalho real para spans. O código não aloca e fica um pouco mais difícil de ler (fatiamento,IndexOf). Compensa em laços sobre entradas grandes, e não em uma tela de configurações. - Um método
Tryprecisa manter um significado estreito. Se ele também engole erros não relacionados (uma referência nula, uma opção inválida), os bugs desaparecem dentro dofalse. Devolvafalsepara a entrada ruim documentada e deixe os erros de programação lançarem.
Documentation Links
- Parse numeric strings, Microsoft Learn
- Parse date and time strings, Microsoft Learn
- Int32.TryParse method, Microsoft Learn
- NumberStyles enum, Microsoft Learn
- IParsable<TSelf> interface, Microsoft Learn
- Enum.TryParse method, Microsoft Learn
- Exceptions and Performance, Framework Design Guidelines, Microsoft Learn