Cómo generar nombres de archivo únicos en C #


131

He implementado un algoritmo que generará nombres únicos para los archivos que se guardarán en el disco duro. Estoy agregando DateTime: horas, minutos, segundos y milisegundos, pero aún así genera nombres duplicados de archivos porque estoy cargando varios archivos a la vez.

¿Cuál es la mejor solución para generar nombres únicos para los archivos que se almacenarán en el disco duro para que no haya 2 archivos iguales?


Depende de otros requisitos; esta [vieja] pregunta era / es demasiado vaga.
user2864740

Respuestas:


240

Si la legibilidad no importa, use GUID .

P.ej:

var myUniqueFileName = string.Format(@"{0}.txt", Guid.NewGuid());

o más corto :

var myUniqueFileName = $@"{Guid.NewGuid()}.txt";

En mis programas, a veces intento, por ejemplo, 10 veces generar un nombre legible ("Image1.png" ... "Image10.png") y si eso falla (porque el archivo ya existe), vuelvo a los GUID.

Actualizar:

Recientemente, también he usado en DateTime.Now.Tickslugar de GUID:

var myUniqueFileName = string.Format(@"{0}.txt", DateTime.Now.Ticks);

o

var myUniqueFileName = $@"{DateTime.Now.Ticks}.txt";

El beneficio para mí es que esto genera un nombre de archivo más corto y "más agradable", en comparación con los GUID.

Tenga en cuenta que, en algunos casos (p. Ej., Al generar muchos nombres aleatorios en muy poco tiempo), esto podría generar valores no únicos.

Apéguese a los GUID si desea asegurarse realmente de que los nombres de los archivos sean únicos, incluso cuando los transfiera a otras computadoras.


77
Me gusta usar Ticks como GUID es realmente feo. También puede obtener un hash de Ticks que reduce la longitud de los caracteres del nombre de archivo. DateTime.Now.Ticks.GetHashCode().ToString("x").ToUpper()
WillMcKill

44
"Ticks" es predecible y no es seguro para subprocesos (ya que se pueden obtener los mismos 'ticks' a partir de múltiples subprocesos / procesos). Esto hace que no sea adecuado para la generación de nombres temporales. Generar X..1..N puede ser adecuado para tareas orientadas al usuario (es decir, copiar en el Explorador), pero es dudoso para el trabajo del servidor.
user2864740

90

Utilizar

Path.GetTempFileName()

o use un nuevo GUID ().

Path.GetTempFilename () en MSDN .


Aquí está el enlace al documento de MSDN: msdn.microsoft.com/en-us/library/…
epotter

3
Sin embargo, GetTempFileName()tenga en cuenta que eso podría generar una excepción si crea muchos de estos archivos sin eliminarlos.
Joey

21
"El método GetTempFileName generará una IOException si se usa para crear más de 65535 archivos sin eliminar archivos temporales anteriores". dice el artículo de MSDN.
Çağdaş Tekin

1
ADVERTENCIA: GetTempFileNamese crea un archivo. Esto también significa que elige la ubicación de la ruta temporal . Por otro lado, GetRandomFileNamees adecuado para generar un nombre de archivo 8.3 que se puede usar con una ruta diferente. (He visto algo de código que utiliza horrible GetTempFileName con un File.Delete sólo para usar el nombre del archivo en otro lugar ..)
user2864740


54

Si la legibilidad del nombre del archivo no es importante, entonces lo hará el GUID, como lo sugieren muchos. Sin embargo, encuentro que buscar en un directorio con 1000 nombres de archivos GUID es muy desalentador para ordenar. Por lo tanto, generalmente uso una combinación de una cadena estática que le da al nombre del archivo cierta información de contexto, una marca de tiempo y GUID.

Por ejemplo:

public string GenerateFileName(string context)
{
    return context + "_" + DateTime.Now.ToString("yyyyMMddHHmmssfff") + "_" + Guid.NewGuid().ToString("N");
}

filename1 = GenerateFileName("MeasurementData");
filename2 = GenerateFileName("Image");

De esta manera, cuando ordeno por nombre de archivo, agrupará automáticamente los archivos por la cadena de contexto y los ordenará por marca de tiempo.

Tenga en cuenta que el límite de nombre de archivo en Windows es 255 caracteres.


1
+1 Para la sugerencia de incluir información útil combinada con un GUID. - Un aparte para tomar con un grano de sal: incluir la fecha y la hora en un nombre de archivo es algo redundante cuando se puede Right Click > Sort By > Date.
Timothy Shields

1
El tiempo se vuelve útil si está almacenando un montón de archivos con diferentes contextos en el mismo directorio. Por supuesto, la generación del nombre del archivo debe ajustarse según sus necesidades específicas.
Mas

Debe ser Guid.NewGuid().ToString();. Falta paréntesis. +1 de lo contrario
Laurent W.

Esto es muy hábil. Marca de tiempo y Guid. +1
JoshYates1980

Me gusta esta solución +1, agregué una segunda extensión de cadena de parámetros que agrego al nombre del archivo, esto respalda aún más la idea de contexto y permite abrir fácilmente el archivo con la aplicación predeterminada al hacer doble clic si es necesario
shelbypereira

23

Aquí hay un algoritmo que devuelve un nombre de archivo legible único basado en el original suministrado. Si el archivo original existe, intentará agregar un índice al nombre del archivo de forma incremental hasta que encuentre uno que no exista. Lee los nombres de archivo existentes en un HashSet para verificar las colisiones, por lo que es bastante rápido (unos cientos de nombres de archivo por segundo en mi máquina), también es seguro para subprocesos y no sufre condiciones de carrera.

Por ejemplo, si lo pasa test.txt, intentará crear archivos en este orden:

test.txt
test (2).txt
test (3).txt

etc. Puede especificar los intentos máximos o simplemente dejarlo en el valor predeterminado.

Aquí hay un ejemplo completo:

class Program
{
    static FileStream CreateFileWithUniqueName(string folder, string fileName, 
        int maxAttempts = 1024)
    {
        // get filename base and extension
        var fileBase = Path.GetFileNameWithoutExtension(fileName);
        var ext = Path.GetExtension(fileName);
        // build hash set of filenames for performance
        var files = new HashSet<string>(Directory.GetFiles(folder));

        for (var index = 0; index < maxAttempts; index++)
        {
            // first try with the original filename, else try incrementally adding an index
            var name = (index == 0)
                ? fileName
                : String.Format("{0} ({1}){2}", fileBase, index, ext);

            // check if exists
            var fullPath = Path.Combine(folder, name);
            if(files.Contains(fullPath))
                continue;

            // try to create the file
            try
            {
                return new FileStream(fullPath, FileMode.CreateNew, FileAccess.Write);
            }
            catch (DirectoryNotFoundException) { throw; }
            catch (DriveNotFoundException) { throw; }
            catch (IOException) 
            {
                // Will occur if another thread created a file with this 
                // name since we created the HashSet. Ignore this and just
                // try with the next filename.
            } 
        }

        throw new Exception("Could not create unique filename in " + maxAttempts + " attempts");
    }

    static void Main(string[] args)
    {
        for (var i = 0; i < 500; i++)
        {
            using (var stream = CreateFileWithUniqueName(@"c:\temp\", "test.txt"))
            {
                Console.WriteLine("Created \"" + stream.Name + "\"");
            }
        }

        Console.ReadKey();
    }
}

hilo seguro ? nostatic readonly variable tampocolock?
Kiquenet

El método en sí es estático, por lo que no comparte nada, por lo que creo que varios subprocesos pueden ingresar este método de manera concurrente de manera segura. Quizás no sea un término correcto para subprocesos: estoy tratando de transmitir que si otro subproceso / proceso crea un archivo con un nombre en conflicto durante la ejecución, este método se recupera y prueba el siguiente nombre disponible. Siéntase libre de editar si cree que se puede mejorar.
Mike Chamberlain

Quizás "no sufrir una condición de carrera" es una mejor manera de expresarlo.
Mike Chamberlain

10

Yo uso GetRandomFileName :

El método GetRandomFileName devuelve una cadena aleatoria criptográficamente fuerte que puede usarse como nombre de carpeta o nombre de archivo. A diferencia de GetTempFileName, GetRandomFileName no crea un archivo. Cuando la seguridad de su sistema de archivos es primordial, este método debe usarse en lugar de GetTempFileName.

Ejemplo:

public static string GenerateFileName(string extension="")
{
    return string.Concat(Path.GetRandomFileName().Replace(".", ""),
        (!string.IsNullOrEmpty(extension)) ? (extension.StartsWith(".") ? extension : string.Concat(".", extension)) : "");
}

¿El método GetRandomFileName () siempre genera el nombre de archivo único cada vez que es similar a GUID ()?
Ashish Shukla

1
@AshishShukla en realidad no tengo idea. msdn dice "se genera una cadena aleatoria criptográficamente fuerte". No tuve problemas hasta ahora. Si la unicidad es crítica, un cheque adicional podría ser una buena idea.
Koray

3
  1. Cree su nombre de archivo con marca de tiempo siguiendo su proceso normal
  2. Verifique si existe el nombre del archivo
  3. Falso: guardar archivo
  4. Verdadero: agregue caracteres adicionales al archivo, tal vez un contador
  5. Ir al paso 2

10
Este algoritmo es vulnerable a la concurrencia
Jader Dias

3

Puede tener un nombre de archivo único generado automáticamente para usted sin ningún método personalizado. Simplemente use lo siguiente con la clase StorageFolder o la clase StorageFile . La clave aquí es: CreationCollisionOption.GenerateUniqueNameyNameCollisionOption.GenerateUniqueName

Para crear un nuevo archivo con un nombre de archivo único:

var myFile = await ApplicationData.Current.LocalFolder.CreateFileAsync("myfile.txt", NameCollisionOption.GenerateUniqueName);

Para copiar un archivo a una ubicación con un nombre de archivo único:

var myFile2 = await myFile1.CopyAsync(ApplicationData.Current.LocalFolder, myFile1.Name, NameCollisionOption.GenerateUniqueName);

Para mover un archivo con un nombre de archivo único en la ubicación de destino:

await myFile.MoveAsync(ApplicationData.Current.LocalFolder, myFile.Name, NameCollisionOption.GenerateUniqueName);

Para cambiar el nombre de un archivo con un nombre de archivo único en la ubicación de destino:

await myFile.RenameAsync(myFile.Name, NameCollisionOption.GenerateUniqueName);

2

He estado usando el siguiente código y funciona bien. Espero que esto te pueda ayudar.

Comienzo con un nombre de archivo único usando una marca de tiempo -

"context_" + DateTime.Now.ToString ("aaaaMMddHHmmssffff")

Código C #

public static string CreateUniqueFile(string logFilePath, string logFileName, string fileExt)
    {
        try
        {
            int fileNumber = 1;

            //prefix with . if not already provided
            fileExt = (!fileExt.StartsWith(".")) ? "." + fileExt : fileExt;

            //Generate new name
            while (File.Exists(Path.Combine(logFilePath, logFileName + "-" + fileNumber.ToString() + fileExt)))
                fileNumber++;

            //Create empty file, retry until one is created
            while (!CreateNewLogfile(logFilePath, logFileName + "-" + fileNumber.ToString() + fileExt))
                fileNumber++;

            return logFileName + "-" + fileNumber.ToString() + fileExt;
        }
        catch (Exception)
        {
            throw;
        }
    }

    private static bool CreateNewLogfile(string logFilePath, string logFile)
    {
        try
        {
            FileStream fs = new FileStream(Path.Combine(logFilePath, logFile), FileMode.CreateNew);
            fs.Close();
            return true;
        }
        catch (IOException)   //File exists, can not create new
        {
            return false;
        }
        catch (Exception)     //Exception occured
        {
            throw;
        }
    }

1

¿Necesita la marca de fecha y hora en el nombre del archivo?

Puede hacer que el nombre de archivo sea un GUID.


@downvoter ¿Alguna razón para el voto negativo? Un GUID para un nombre de archivo parece ser una respuesta popular a esta pregunta.
Larry Hipp

Es una respuesta repetida, y no tengo suficiente reputación para votar en contra
Jader Dias el

@XMLforDummies mi respuesta fue una de las primeras. Puede que ahora no lo parezca porque solo muestra las horas. Es una respuesta repetida porque probablemente sea la respuesta correcta.
Larry Hipp


1

¿Qué tal usar Guid.NewGuid()para crear un GUID y usarlo como nombre de archivo (o parte del nombre de archivo junto con su marca de tiempo si lo desea)?


1

He escrito una función recursiva simple que genera nombres de archivo como lo hace Windows, al agregar un número de secuencia antes de la extensión del archivo.

Dada una ruta de archivo deseada de C:\MyDir\MyFile.txt, y el archivo ya existe, devuelve una ruta de archivo final de C:\MyDir\MyFile_1.txt.

Se llama así:

var desiredPath = @"C:\MyDir\MyFile.txt";
var finalPath = UniqueFileName(desiredPath);

private static string UniqueFileName(string path, int count = 0)
{
    if (count == 0)
    {
        if (!File.Exists(path))
        {
            return path;
        }
    }
    else
    {
        var candidatePath = string.Format(
            @"{0}\{1}_{2}{3}",
            Path.GetDirectoryName(path),
            Path.GetFileNameWithoutExtension(path),
            count,
            Path.GetExtension(path));

        if (!File.Exists(candidatePath))
        {
            return candidatePath;
        }
    }

    count++;
    return UniqueFileName(path, count);
}

Esto no es seguro para subprocesos ni para procesos. Hay una condición de carrera con la comprobación File.Exists y cualquier creación (supuestamente posterior) del archivo. Trivialmente, cuando se llama dos veces seguidas sin crear un archivo, devolverá el mismo resultado.
user2864740

1

¿Por qué no podemos hacer una identificación única como se muestra a continuación?

Podemos usar DateTime.Now.Ticks y Guid.NewGuid (). ToString () para combinar y crear una identificación única.

A medida que se agregan DateTime.Now.Ticks, podemos encontrar la fecha y la hora en segundos en los que se crea la identificación única.

Por favor vea el código.

var ticks = DateTime.Now.Ticks;
var guid = Guid.NewGuid().ToString();
var uniqueSessionId = ticks.ToString() +'-'+ guid; //guid created by combining ticks and guid

var datetime = new DateTime(ticks);//for checking purpose
var datetimenow = DateTime.Now;    //both these date times are different.

Incluso podemos tomar parte de los ticks en una identificación única y verificar la fecha y hora posteriores para futuras referencias.

Puede adjuntar la identificación única creada al nombre del archivo o puede usarse para crear una identificación de sesión única para iniciar sesión y cerrar sesión en nuestra aplicación o sitio web.


¿Por qué molestarse con 'ticks' si hay un GUID?
user2864740

Durante cualquier escenario, si necesita verificar cuándo se genera el uniqueSessionId, obtendrá la hora exacta. Y también en ese tic específico ocurrirá solo una vez en la vida.
Jineesh Uvantavida

Trivialmente, esa suposición acerca de los ticks no es válida: 1) múltiples observadores pueden ver el mismo 'tick' (pensar hilos / procesos) y 2) el mismo observador puede observar el mismo 'tick' varias veces, si se les consulta con la suficiente rapidez.
user2864740

Sin embargo, por simplemente usando Guid.NewGuid (e ignorando el hecho de que no es "criptográficamente aleatoria", que puede ser de interés en algunos casos), podemos afirmar que, con una probabilidad bastante alta de que no se preocupan por lo contrario, un identificador único a generarse: esta es una garantía mucho más alta que los 'ticks' . Por lo tanto, los 'ticks' no tienen valor / uso aquí, ya que los datos "secundarios" se introducen en el nombre del archivo.
usuario2864740

(FWIW: Acabo de arreglar un código con una afirmación rota mencionada anteriormente sobre 'tiempo único' ...)
usuario2864740

0

Si desea tener la fecha, hora, minutos, etc., puede usar una variable estática. Agregue el valor de esta variable al nombre del archivo. Puede iniciar el contador con 0 e incrementar cuando haya creado un archivo. De esta forma, el nombre de archivo seguramente será único, ya que también tiene segundos en el archivo.


0

Usualmente hago algo en este sentido:

  • comenzar con un nombre de archivo madre ( work.dat1por ejemplo)
  • intenta crearlo con CreateNew
  • si eso funciona, tienes el archivo, de lo contrario ...
  • mezclar la fecha / hora actual con el nombre del archivo ( work.2011-01-15T112357.datpor ejemplo)
  • intenta crear el archivo
  • si eso funcionó, tienes el archivo, de lo contrario ...
  • Mezcle un contador monótono en el nombre del archivo ( work.2011-01-15T112357.0001.datpor ejemplo. (No me gustan los GUID. Prefiero el orden / previsibilidad).
  • Intenta crear el archivo. Sigue marcando el contador y vuelve a intentarlo hasta que se cree un archivo para ti.

Aquí hay una clase de muestra:

static class DirectoryInfoHelpers
{
    public static FileStream CreateFileWithUniqueName( this DirectoryInfo dir , string rootName )
    {
        FileStream fs = dir.TryCreateFile( rootName ) ; // try the simple name first

        // if that didn't work, try mixing in the date/time
        if ( fs == null )
        {
            string date = DateTime.Now.ToString( "yyyy-MM-ddTHHmmss" ) ;
            string stem = Path.GetFileNameWithoutExtension(rootName) ;
            string ext  = Path.GetExtension(rootName) ?? ".dat" ;

            ext = ext.Substring(1);

            string fn = string.Format( "{0}.{1}.{2}" , stem , date , ext ) ;
            fs = dir.TryCreateFile( fn ) ;

            // if mixing in the date/time didn't work, try a sequential search
            if ( fs == null )
            {
                int seq = 0 ;
                do
                {
                    fn = string.Format( "{0}.{1}.{2:0000}.{3}" , stem , date , ++seq , ext ) ;
                    fs = dir.TryCreateFile( fn ) ;
                } while ( fs == null ) ;
            }

        }

        return fs ;
    }

    private static FileStream TryCreateFile(this DirectoryInfo dir , string fileName )
    {
        FileStream fs = null ;
        try
        {
            string fqn = Path.Combine( dir.FullName , fileName ) ;

            fs = new FileStream( fqn , FileMode.CreateNew , FileAccess.ReadWrite , FileShare.None ) ;
        }
        catch ( Exception )
        {
            fs = null ;
        }
        return fs ;
    }

}

Es posible que desee ajustar el algoritmo (siempre use todos los componentes posibles para el nombre del archivo, por ejemplo). Depende del contexto: si estaba creando archivos de registro, por ejemplo, si quisiera rotar fuera de existencia, querría que todos compartieran el mismo patrón para el nombre.

El código no es perfecto (no se verifican los datos pasados, por ejemplo). Y el algoritmo no es perfecto (si llena el disco duro o encuentra permisos, errores de E / S reales u otros errores del sistema de archivos, por ejemplo, esto se bloqueará, tal como está, en un bucle infinito).


0

Termino concatenando GUID con cadena de día mes año segundo milisegundo y creo que esta solución es bastante buena en mi escenario



0

Escribí una clase específicamente para hacer esto. Se inicializa con una parte "base" (el valor predeterminado es una marca de tiempo con un minuto exacto) y luego agrega letras para crear nombres únicos. Entonces, si el primer sello generado es 1907101215a, el segundo sería 1907101215b, luego 1907101215c, etc.

Si necesito más de 25 sellos únicos, entonces uso las 'z' unitarias para contar los 25. Entonces, va 1907101215y, 1907101215za, 1907101215zb, ... 1907101215zy, 1907101215zza, 1907101215zzb, y así sucesivamente. Esto garantiza que los sellos siempre se ordenarán alfanuméricamente en el orden en que se generaron (siempre que el siguiente carácter después del sello no sea una letra).

No es seguro para subprocesos, no actualiza automáticamente el tiempo y se hincha rápidamente si necesita cientos de sellos, pero creo que es suficiente para mis necesidades.

/// <summary>
/// Class for generating unique stamps (for filenames, etc.)
/// </summary>
/// <remarks>
/// Each time ToString() is called, a unique stamp is generated.
/// Stamps are guaranteed to sort alphanumerically in order of generation.
/// </remarks>
public class StampGenerator
{
  /// <summary>
  /// All the characters which could be the last character in the stamp.
  /// </summary>
  private static readonly char[] _trailingChars =
  {
    'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
    'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
    'u', 'v', 'w', 'x', 'y'
  };

  /// <summary>
  /// How many valid trailing characters there are.
  /// </summary>
  /// <remarks>Should always equal _trailingChars.Length</remarks>
  public const int TRAILING_RANGE = 25;

  /// <summary>
  /// Maximum length of the stamp. Hard-coded for laziness.
  /// </summary>
  public const int MAX_LENGTH_STAMP = 28;

  /// <summary>
  /// Base portion of the stamp. Will be constant between calls.
  /// </summary>
  /// <remarks>
  /// This is intended to uniquely distinguish between instances.
  /// Default behavior is to generate a minute-accurate timestamp.
  /// </remarks>
  public string StampBase { get; }

  /// <summary>
  /// Number of times this instance has been called.
  /// </summary>
  public int CalledTimes { get; private set; }

  /// <summary>
  /// Maximum number of stamps that can be generated with a given base.
  /// </summary>
  public int MaxCalls { get; }

  /// <summary>
  /// Number of stamps remaining for this instance.
  /// </summary>
  public int RemainingCalls { get { return MaxCalls - CalledTimes; } }

  /// <summary>
  /// Instantiate a StampGenerator with a specific base.
  /// </summary>
  /// <param name="stampBase">Base of stamp.</param>
  /// <param name="calledTimes">
  /// Number of times this base has already been used.
  /// </param>
  public StampGenerator(string stampBase, int calledTimes = 0)
  {
    if (stampBase == null)
    {
      throw new ArgumentNullException("stampBase");
    }
    else if (Regex.IsMatch(stampBase, "[^a-zA-Z_0-9 \\-]"))
    {
      throw new ArgumentException("Invalid characters in Stamp Base.",
                                  "stampBase");
    }
    else if (stampBase.Length >= MAX_LENGTH_STAMP - 1)
    {
      throw new ArgumentException(
        string.Format("Stamp Base too long. (Length {0} out of {1})",
                      stampBase.Length, MAX_LENGTH_STAMP - 1), "stampBase");
    }
    else if (calledTimes < 0)
    {
      throw new ArgumentOutOfRangeException(
        "calledTimes", calledTimes, "calledTimes cannot be negative.");
    }
    else
    {
      int maxCalls = TRAILING_RANGE * (MAX_LENGTH_STAMP - stampBase.Length);
      if (calledTimes >= maxCalls)
      {
        throw new ArgumentOutOfRangeException(
          "calledTimes", calledTimes, string.Format(
            "Called Times too large; max for stem of length {0} is {1}.",
            stampBase.Length, maxCalls));
      }
      else
      {
        StampBase = stampBase;
        CalledTimes = calledTimes;
        MaxCalls = maxCalls;
      }
    }
  }

  /// <summary>
  /// Instantiate a StampGenerator with default base string based on time.
  /// </summary>
  public StampGenerator() : this(DateTime.Now.ToString("yMMddHHmm")) { }

  /// <summary>
  /// Generate a unique stamp.
  /// </summary>
  /// <remarks>
  /// Stamp values are orered like this:
  /// a, b, ... x, y, za, zb, ... zx, zy, zza, zzb, ...
  /// </remarks>
  /// <returns>A unique stamp.</returns>
  public override string ToString()
  {
    int zCount = CalledTimes / TRAILING_RANGE;
    int trailing = CalledTimes % TRAILING_RANGE;
    int length = StampBase.Length + zCount + 1;

    if (length > MAX_LENGTH_STAMP)
    {
      throw new InvalidOperationException(
        "Stamp length overflown! Cannot generate new stamps.");
    }
    else
    {
      CalledTimes = CalledTimes + 1;
      var builder = new StringBuilder(StampBase, length);
      builder.Append('z', zCount);
      builder.Append(_trailingChars[trailing]);
      return builder.ToString();
    }
  }
}

-1

DateTime.Now.Ticksno es seguro, Guid.NewGuid()es demasiado feo, si necesita algo limpio y casi seguro ( no es 100% seguro, por ejemplo, si lo llama 1,000,000 de veces en 1 ms ), intente:

Math.Abs(Guid.NewGuid().GetHashCode())

Por seguro quiero decir seguro para ser único cuando lo llamas tantas veces en un período muy corto de pocos ms de tiempo.


¿Hay algún problema con mi solución de downvoter? Por favor hagamelo saber.
Mehdi Dehghani

El GetHashCodemétodo devuelve un int, que tiene un rango de 32 bits, mientras que a GUIDtiene un rango de 128 bits, por lo que es mucho más probable que sea único. Si no le gusta el formato de un GUIDvalor, simplemente instálelo ToString("N"), lo que elimina los guiones.
4thex
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.