Práticas recomendadas para exibir e persistir dados formatados

Este artigo examina como dados formatados, como dados numéricos e dados de data e hora, são manipulados para exibição e armazenamento.

Ao desenvolver com .NET, use a formatação que leva em conta a cultura para exibir dados que não são de cadeias de caracteres, como números e datas, em uma interface do usuário. Use a formatação com a cultura invariável para persistir os dados que não são de cadeias de caracteres no formato de cadeia de caracteres. Não use a formatação que leva em conta a cultura para persistir dados numéricos ou dados de data e hora no formato de cadeia de caracteres.

Exibir dados formatados

Quando você exibir dados que não são de cadeias de caracteres como números e datas e horas para os usuários, formate-os usando as configurações culturais do usuário. Por padrão, todos os seguintes itens usam a cultura atual em operações de formatação:

  • Cadeias de caracteres interpoladas compatíveis com os compiladores C# e Visual Basic.
  • Operações de concatenação de cadeias de caracteres que usam os operadores de concatenação do C# ou Visual Basic, ou que chamam o método String.Concat diretamente.
  • O método String.Format.
  • Os métodos ToString dos tipos numéricos e dos tipos de data e hora.

Para especificar explicitamente que uma cadeia de caracteres deve ser formatada usando as convenções de uma cultura específica ou a cultura invariável, você pode fazer o seguinte:

  • Ao usar os métodos String.Format e ToString, chame uma sobrecarga que tenha um parâmetro provider, tal como String.Format(IFormatProvider, String, Object[]) ou DateTime.ToString(IFormatProvider), e passe a ela a propriedade CultureInfo.CurrentCulture, a propriedade CultureInfo.InvariantCulture ou uma instância de CultureInfo que represente a cultura desejada.

  • Para concatenação de cadeias de caracteres, não permita que o compilador execute nenhuma conversão implícita. Em vez disso, execute uma conversão explícita, chamando uma sobrecarga ToString que tenha um parâmetro provider. Por exemplo, o compilador usa implicitamente a cultura atual ao converter um valor Double em uma cadeia de caracteres no seguinte código C#:

    string concat1 = "The amount is " + 126.03 + ".";
    Console.WriteLine(concat1);
    
    Dim concat1 As String = "The amount is " & 126.03 & "."
    Console.WriteLine(concat1)
    

    Em vez disso, é possível especificar explicitamente a cultura cujas convenções de formatação são usadas na conversão ao chamar o método Double.ToString(IFormatProvider), assim como ocorre com o seguinte código:

    string concat2 = "The amount is " + 126.03.ToString(CultureInfo.InvariantCulture) + ".";
    Console.WriteLine(concat2);
    
    Dim concat2 As String = "The amount is " & 126.03.ToString(CultureInfo.InvariantCulture) & "."
    Console.WriteLine(concat2)
    
  • Para a interpolação de cadeia de caracteres, em vez de atribuir uma cadeia de caracteres interpolada a uma instância de String, atribua-a a um FormattableString. Em seguida, você pode chamar seu método FormattableString.ToString() para produzir uma cadeia de caracteres de resultados que reflita as convenções da cultura atual ou pode chamar o método FormattableString.ToString(IFormatProvider) para produzir uma cadeia de caracteres de resultados que reflita as convenções de uma cultura específica.

    Você também pode passar a cadeia de caracteres formatável para o método FormattableString.Invariant estático para produzir uma cadeia de caracteres de resultado que reflete as convenções da cultura invariável. O exemplo a seguir ilustra esta abordagem. (A saída do exemplo reflete uma cultura atual de en-US.)

    using System;
    using System.Globalization;
    
    class Program
    {
        static void Main()
        {
            Decimal value = 126.03m;
            FormattableString amount = $"The amount is {value:C}";
            Console.WriteLine(amount.ToString());
            Console.WriteLine(amount.ToString(new CultureInfo("fr-FR")));
            Console.WriteLine(FormattableString.Invariant(amount));
        }
    }
    // The example displays the following output:
    //    The amount is $126.03
    //    The amount is 126,03 €
    //    The amount is ¤126.03
    
    Imports System.Globalization
    
    Module Program
        Sub Main()
            Dim value As Decimal = 126.03
            Dim amount As FormattableString = $"The amount is {value:C}" 
            Console.WriteLine(amount.ToString())
            Console.WriteLine(amount.ToString(new CultureInfo("fr-FR")))
            Console.WriteLine(FormattableString.Invariant(amount))
        End Sub
    End Module
    ' The example displays the following output:
    '    The amount is $126.03
    '    The amount is 126,03 €
    '    The amount is ¤126.03
    

    Observação

    Se você estiver usando C# e formatando usando a cultura invariante, é mais eficiente chamar String.Create(IFormatProvider, DefaultInterpolatedStringHandler) e passar CultureInfo.InvariantCulture para o primeiro parâmetro. Para obter mais informações, confira Interpolação de cadeias de caracteres em C# 10 e .NET 6.

Manter dados formatados

Você pode manter os dados que não são de cadeias de caracteres como dados binários ou como dados formatados. Se optar por salvá-los como dados formatados, você deverá chamar uma sobrecarga de método de formatação que inclua um parâmetro provider e passar para ele a propriedade CultureInfo.InvariantCulture. A cultura invariável fornece um formato consistente para os dados formatados que é independente da cultura e do computador. Em contraste, dados persistentes que são formatados usando culturas diferentes da cultura invariável têm várias limitações:

  • É provável que os dados não sejam utilizáveis se forem recuperados em um sistema que tem uma cultura diferente ou se o usuário do sistema atual alterar a cultura atual e tentar recuperar os dados.
  • As propriedades de uma cultura em um computador específico podem ser diferentes dos valores padrão. A qualquer momento, um usuário pode personalizar as configurações de exibição que levam em conta a cultura. Devido a isso, os dados formatados que são salvos em um sistema podem não ser legíveis após o usuário personalizar as configurações culturais. É provável que a portabilidade dos dados formatados entre computadores seja ainda mais limitada.
  • Os padrões internacionais, regionais ou nacionais que controlam a formatação de números ou datas e horas são alterados ao longo do tempo e essas alterações são incorporadas nas atualizações do sistema operacional Windows. Quando as convenções de formatação mudam, os dados que foram formatados usando as convenções anteriores podem se tornar ilegíveis.

O exemplo a seguir ilustra a portabilidade limitada resultante do uso da formatação que leva em conta a cultura para manter os dados. O exemplo salva uma matriz de valores de data e hora em um arquivo. Eles são formatados usando as convenções da cultura do inglês (Estados Unidos). Depois que o aplicativo altera a cultura atual para francês (Suíça), ele tenta ler os valores salvos usando as convenções de formatação da cultura atual. A tentativa de ler dois dos itens de dados gera uma exceção FormatException e a matriz de datas agora contém dois elementos incorretos que são iguais a MinValue.

using System;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading;

public class Example
{
   private static string filename = @".\dates.dat";

   public static void Main()
   {
      DateTime[] dates = { new DateTime(1758, 5, 6, 21, 26, 0),
                           new DateTime(1818, 5, 5, 7, 19, 0),
                           new DateTime(1870, 4, 22, 23, 54, 0),
                           new DateTime(1890, 9, 8, 6, 47, 0),
                           new DateTime(1905, 2, 18, 15, 12, 0) };
      // Write the data to a file using the current culture.
      WriteData(dates);
      // Change the current culture.
      Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("fr-CH");
      // Read the data using the current culture.
      DateTime[] newDates = ReadData();
      foreach (var newDate in newDates)
         Console.WriteLine(newDate.ToString("g"));
   }

   private static void WriteData(DateTime[] dates)
   {
      StreamWriter sw = new StreamWriter(filename, false, Encoding.UTF8);
      for (int ctr = 0; ctr < dates.Length; ctr++) {
         sw.Write("{0}", dates[ctr].ToString("g", CultureInfo.CurrentCulture));
         if (ctr < dates.Length - 1) sw.Write("|");
      }
      sw.Close();
   }

   private static DateTime[] ReadData()
   {
      bool exceptionOccurred = false;

      // Read file contents as a single string, then split it.
      StreamReader sr = new StreamReader(filename, Encoding.UTF8);
      string output = sr.ReadToEnd();
      sr.Close();

      string[] values = output.Split( new char[] { '|' } );
      DateTime[] newDates = new DateTime[values.Length];
      for (int ctr = 0; ctr < values.Length; ctr++) {
         try {
            newDates[ctr] = DateTime.Parse(values[ctr], CultureInfo.CurrentCulture);
         }
         catch (FormatException) {
            Console.WriteLine("Failed to parse {0}", values[ctr]);
            exceptionOccurred = true;
         }
      }
      if (exceptionOccurred) Console.WriteLine();
      return newDates;
   }
}
// The example displays the following output:
//       Failed to parse 4/22/1870 11:54 PM
//       Failed to parse 2/18/1905 3:12 PM
//
//       05.06.1758 21:26
//       05.05.1818 07:19
//       01.01.0001 00:00
//       09.08.1890 06:47
//       01.01.0001 00:00
//       01.01.0001 00:00
Imports System.Globalization
Imports System.IO
Imports System.Text
Imports System.Threading

Module Example
    Private filename As String = ".\dates.dat"

    Public Sub Main()
        Dim dates() As Date = {#5/6/1758 9:26PM#, #5/5/1818 7:19AM#, _
                                #4/22/1870 11:54PM#, #9/8/1890 6:47AM#, _
                                #2/18/1905 3:12PM#}
        ' Write the data to a file using the current culture.
        WriteData(dates)
        ' Change the current culture.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("fr-CH")
        ' Read the data using the current culture.
        Dim newDates() As Date = ReadData()
        For Each newDate In newDates
            Console.WriteLine(newDate.ToString("g"))
        Next
    End Sub

    Private Sub WriteData(dates() As Date)
        Dim sw As New StreamWriter(filename, False, Encoding.Utf8)
        For ctr As Integer = 0 To dates.Length - 1
            sw.Write("{0}", dates(ctr).ToString("g", CultureInfo.CurrentCulture))
            If ctr < dates.Length - 1 Then sw.Write("|")
        Next
        sw.Close()
    End Sub

    Private Function ReadData() As Date()
        Dim exceptionOccurred As Boolean = False

        ' Read file contents as a single string, then split it.
        Dim sr As New StreamReader(filename, Encoding.Utf8)
        Dim output As String = sr.ReadToEnd()
        sr.Close()

        Dim values() As String = output.Split({"|"c})
        Dim newDates(values.Length - 1) As Date
        For ctr As Integer = 0 To values.Length - 1
            Try
                newDates(ctr) = DateTime.Parse(values(ctr), CultureInfo.CurrentCulture)
            Catch e As FormatException
                Console.WriteLine("Failed to parse {0}", values(ctr))
                exceptionOccurred = True
            End Try
        Next
        If exceptionOccurred Then Console.WriteLine()
        Return newDates
    End Function
End Module
' The example displays the following output:
'       Failed to parse 4/22/1870 11:54 PM
'       Failed to parse 2/18/1905 3:12 PM
'       
'       05.06.1758 21:26
'       05.05.1818 07:19
'       01.01.0001 00:00
'       09.08.1890 06:47
'       01.01.0001 00:00
'       01.01.0001 00:00
'

No entanto, se você substituir a propriedade CultureInfo.CurrentCulture por CultureInfo.InvariantCulture em chamadas para DateTime.ToString(String, IFormatProvider) e DateTime.Parse(String, IFormatProvider), os dados persistentes de data e hora serão restaurados com êxito, como na seguinte saída:

06.05.1758 21:26
05.05.1818 07:19
22.04.1870 23:54
08.09.1890 06:47
18.02.1905 15:12