¿Cuál es el peor problema en C # o .NET? [cerrado]


377

Recientemente estuve trabajando con un DateTimeobjeto y escribí algo como esto:

DateTime dt = DateTime.Now;
dt.AddDays(1);
return dt; // still today's date! WTF?

La documentación de intellisense AddDays()dice que agrega un día a la fecha, lo cual no es así; en realidad, devuelve una fecha con un día agregado, por lo que debe escribirlo como:

DateTime dt = DateTime.Now;
dt = dt.AddDays(1);
return dt; // tomorrow's date

Este me ha mordido varias veces antes, así que pensé que sería útil catalogar las peores trampas de C #.


157
return DateTime.Now.AddDays (1);
crashmstr

23
AFAIK, los tipos de valores incorporados son todos inmutables, al menos en que cualquier método incluido con el tipo devuelve un nuevo elemento en lugar de modificar el elemento existente. Al menos, no puedo pensar en uno fuera de mi cabeza que no haga esto: todo agradable y consistente.
Joel Coehoorn

66
Tipo de valor mutable: System.Collections.Generics.List.Enumerator :( (Y sí, puede ver que se comporta de manera extraña si se esfuerza lo suficiente.)
Jon Skeet

13
El intellisense le brinda toda la información que necesita. Dice que devuelve un objeto DateTime. Si solo modificara el que pasó, sería un método nulo.
John Kraft

20
No necesariamente: StringBuilder.Append (...) devuelve "esto" por ejemplo. Eso es bastante común en las interfaces fluidas.
Jon Skeet

Respuestas:


304
private int myVar;
public int MyVar
{
    get { return MyVar; }
}

Blammo Su aplicación se bloquea sin seguimiento de pila. Pasa todo el tiempo.

(Observe el capital en MyVarlugar de minúsculas myVaren el captador).


112
y TAN apropiado para este sitio :)
gbjbaanb

6262
Pongo guiones bajos en el miembro privado, ¡ayuda mucho!
chakrit

61
Uso las propiedades automáticas donde puedo, detiene mucho este tipo de problema;)
TWith2Sugars

28
Esta es una GRAN razón para usar prefijos para sus campos privados (hay otros, pero este es uno bueno): _myVar, m_myVar
jrista

205
@jrista: O favor NO ... no m_ ... aargh el horror ...
fretje

254

Type.GetType

El que he visto morder a mucha gente es Type.GetType(string). Se preguntan por qué funciona para los tipos en su propio ensamblaje, y algunos tipos les gusta System.String, pero no System.Windows.Forms.Form. La respuesta es que solo se ve en el ensamblaje actual y en mscorlib.


Métodos anónimos

C # 2.0 introdujo métodos anónimos, lo que lleva a situaciones desagradables como esta:

using System;
using System.Threading;

class Test
{
    static void Main()
    {
        for (int i=0; i < 10; i++)
        {
            ThreadStart ts = delegate { Console.WriteLine(i); };
            new Thread(ts).Start();
        }
    }
}

¿Qué imprimirá eso? Bueno, depende completamente de la programación. Imprimirá 10 números, pero probablemente no imprimirá 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, que es lo que puede esperar. El problema es que es la ivariable que se ha capturado, no su valor en el momento de la creación del delegado. Esto se puede resolver fácilmente con una variable local adicional del alcance correcto:

using System;
using System.Threading;

class Test
{
    static void Main()
    {
        for (int i=0; i < 10; i++)
        {
            int copy = i;
            ThreadStart ts = delegate { Console.WriteLine(copy); };
            new Thread(ts).Start();
        }
    }
}

Ejecución diferida de bloques iteradores

Esta "prueba de unidad del pobre" no pasa, ¿por qué no?

using System;
using System.Collections.Generic;
using System.Diagnostics;

class Test
{
    static IEnumerable<char> CapitalLetters(string input)
    {
        if (input == null)
        {
            throw new ArgumentNullException(input);
        }
        foreach (char c in input)
        {
            yield return char.ToUpper(c);
        }
    }

    static void Main()
    {
        // Test that null input is handled correctly
        try
        {
            CapitalLetters(null);
            Console.WriteLine("An exception should have been thrown!");
        }
        catch (ArgumentNullException)
        {
            // Expected
        }
    }
}

La respuesta es que el código dentro de la fuente del CapitalLetterscódigo no se ejecuta hasta MoveNext()que se llama por primera vez al método del iterador .

Tengo algunas otras rarezas en mi página de acertijos .


25
¡El ejemplo del iterador es tortuoso!
Jimmy

8
¿por qué no dividir esto en 3 respuestas para que podamos votar cada una en lugar de todas juntas?
chakrit

13
@chakrit: En retrospectiva, probablemente habría sido una buena idea, pero creo que ya es demasiado tarde. También podría haber parecido que solo estaba tratando de obtener más rep ...
Jon Skeet

19
En realidad, Type.GetType funciona si proporciona el AssemblyQualifiedName. Type.GetType ("System.ServiceModel.EndpointNotFoundException, System.ServiceModel, Version = 3.0.0.0, Culture = neutral, PublicKeyToken = b77a5c561934e089");
chilltemp

2
@kentaromiura: la resolución de sobrecarga comienza en el tipo más derivado y funciona en el árbol, pero solo mira los métodos declarados originalmente en el tipo que está mirando. Foo (int) anula el método base, por lo que no se considera. Foo (objeto) es aplicable, por lo que la resolución de sobrecarga se detiene allí. Extraño, lo sé.
Jon Skeet

194

Relanzando excepciones

Un truco que tiene muchos desarrolladores nuevos es la semántica de excepción de relanzamiento.

Mucho tiempo veo código como el siguiente

catch(Exception e) 
{
   // Do stuff 
   throw e; 
}

El problema es que borra el seguimiento de la pila y hace que el diagnóstico de problemas sea mucho más difícil, ya que no puede rastrear dónde se originó la excepción.

El código correcto es la instrucción throw sin argumentos:

catch(Exception)
{
    throw;
}

O envolviendo la excepción en otra, y usando la excepción interna para obtener el seguimiento de la pila original:

catch(Exception e) 
{
   // Do stuff 
   throw new MySpecialException(e); 
}

Afortunadamente, alguien me enseñó sobre esto en mi primera semana y lo encuentro en el código de desarrolladores más senior. Es: catch () {throw; } ¿Lo mismo que el segundo fragmento de código? atrapar (Excepción e) {lanzar; } solo que no crea un objeto de Excepción y lo llena?
StuperUser

Además del error de usar throw ex (o throw e) en lugar de solo throw, tengo que preguntarme qué casos hay cuando vale la pena atrapar una excepción solo para lanzarlo nuevamente.
Ryan Lundy

13
@ Kyralessa: hay muchos casos: por ejemplo, si desea revertir una transacción, antes de que la persona que llama obtenga la excepción. Retrocedes y luego vuelves a tirar.
R. Martinho Fernandes

77
Veo esto todo el tiempo donde las personas atrapan y vuelven a lanzar excepciones solo porque se les enseña que deben atrapar todas las excepciones, sin darse cuenta de que será atrapado más arriba en la pila de llamadas. Me vuelve loco.
James Westgate,

55
@ Kyralessa el caso más grande es cuando tienes que hacer un registro. Registre el error en catch y
vuelva a

194

La ventana de vigilancia de Heisenberg

Esto puede morderte mal si estás haciendo cosas de carga a pedido, como esta:

private MyClass _myObj;
public MyClass MyObj {
  get {
    if (_myObj == null)
      _myObj = CreateMyObj(); // some other code to create my object
    return _myObj;
  }
}

Ahora digamos que tiene algún código en otro lugar usando esto:

// blah
// blah
MyObj.DoStuff(); // Line 3
// blah

Ahora quieres depurar tu CreateMyObj()método. Entonces pones un punto de interrupción en la línea 3 anterior, con la intención de ingresar al código. Solo por si acaso, también pones un punto de interrupción en la línea de arriba que dice _myObj = CreateMyObj();, e incluso un punto de interrupción dentro de CreateMyObj()sí mismo.

El código llega a su punto de interrupción en la línea 3. Entras en el código. Espera ingresar el código condicional, porque _myObjobviamente es nulo, ¿verdad? Uh ... entonces ... ¿por qué se saltó la condición y se fue directamente return _myObj? Pasa el mouse sobre _myObj ... y, de hecho, ¡tiene un valor! ¡¿Cómo pasó eso?!

La respuesta es que su IDE provocó que obtuviera un valor, porque tiene abierta una ventana de "observación", especialmente la ventana de observación "Autos", que muestra los valores de todas las variables / propiedades relevantes para la línea de ejecución actual o anterior. Cuando llegaste a tu punto de interrupción en la línea 3, la ventana del reloj decidió que te interesaría saber el valor de MyObj, por lo que detrás de escena, ignorando cualquiera de tus puntos de interrupción , se fue y calculó el valor MyObjpara ti, incluida la llamada a CreateMyObj()ese establece el valor de _myObj!

Es por eso que llamo a esto la Ventana de vigilancia de Heisenberg: no se puede observar el valor sin afectarlo ... :)

GOTCHA!


Editar : creo que el comentario de @ ChristianHayter merece ser incluido en la respuesta principal, porque parece una solución efectiva para este problema. Entonces, cada vez que tenga una propiedad con carga lenta ...

Decora tu propiedad con [DebuggerBrowsable (DebuggerBrowsableState.Never)] o [DebuggerDisplay ("<cargado bajo demanda>")]. - Christian Hayter


10
brillante hallazgo! No eres un programador, eres un verdadero depurador.
esto. __curious_geek

26
Me he encontrado con esto incluso al pasar el mouse sobre la variable, no solo la ventana de observación.
Richard Morgan

31
Decora tu propiedad con [DebuggerBrowsable(DebuggerBrowsableState.Never)]o [DebuggerDisplay("<loaded on demand>")].
Christian Hayter

44
Si está desarrollando una clase de marco y desea ver la funcionalidad de la ventana de observación sin alterar el comportamiento en tiempo de ejecución de una propiedad construida con pereza, puede usar un proxy de tipo depurador para devolver el valor si ya se ha construido, y un mensaje de que la propiedad no construido si ese es el caso. La Lazy<T>clase (en particular por su Valuepropiedad) es un ejemplo de dónde se usa esto.
Sam Harwell

44
Recuerdo a alguien que (por alguna razón no puedo entender) cambió el valor del objeto en una sobrecarga de ToString. Cada vez que se cernía sobre él, la información sobre herramientas le daba un valor diferente: no podía entenderlo ...
JNF

144

Aquí hay otro momento que me atrapa:

static void PrintHowLong(DateTime a, DateTime b)
{
    TimeSpan span = a - b;
    Console.WriteLine(span.Seconds);        // WRONG!
    Console.WriteLine(span.TotalSeconds);   // RIGHT!
}

TimeSpan.Seconds es la porción de segundos del intervalo de tiempo (2 minutos y 0 segundos tiene un valor de segundos de 0).

TimeSpan.TotalSeconds es el intervalo de tiempo completo medido en segundos (2 minutos tiene un valor total de segundos de 120).


1
Sí, ese también me tiene a mí. Creo que debería ser TimeSpan.SecondsPart o algo para dejar más claro lo que representa.
Dan Diplo

3
Al releer esto, me pregunto por qué TimeSpanincluso tiene una Secondspropiedad. ¿Quién le importa a una rata cuál es la porción de segundos de un intervalo de tiempo, de todos modos? Es un valor arbitrario, dependiente de la unidad; No puedo concebir ningún uso práctico para ello.
MusiGenesis

2
Tiene sentido para mí que TimeSpan.TotalSeconds devolvería ... el número total de segundos en el lapso de tiempo.
Ed S.

16
@MusiGenesis la propiedad es útil. ¿Qué sucede si quiero mostrar un intervalo de tiempo dividido en pedazos? Por ejemplo, supongamos que su intervalo de tiempo representa la duración de '3 horas 15 minutos 10 segundos'. ¿Cómo puede acceder a esta información sin las propiedades de Segundos, Horas, Minutos?
SolutionYogi

1
En API similares, he usado SecondsParty SecondsTotalpara distinguir los dos.
BlueRaja - Danny Pflughoeft

80

Pérdida de memoria porque no desenganchó eventos.

Esto incluso atrapó a algunos desarrolladores senior que conozco.

Imagine un formulario WPF con muchas cosas en él, y en algún lugar allí se suscribe a un evento. Si no cancela la suscripción, todo el formulario se guarda en la memoria después de cerrarse y desreferenciarse.

¡Creo que el problema que vi fue crear un DispatchTimer en el formulario WPF y suscribirse al evento Tick, si no haces un - = en el temporizador tu formulario pierde memoria!

En este ejemplo, su código de desmontaje debería tener

timer.Tick -= TimerTickEventHandler;

Este es especialmente complicado ya que creó la instancia del DispatchTimer dentro del formulario WPF, por lo que podría pensar que sería una referencia interna manejada por el proceso de Recolección de Basura ... desafortunadamente, el DispatchTimer usa una lista interna estática de suscripciones y servicios solicitudes en el subproceso de la interfaz de usuario, por lo que la referencia es 'propiedad' de la clase estática.


1
El truco consiste en liberar siempre todas las suscripciones de eventos que cree. Si comienza a confiar en que los Formularios lo hacen por usted, puede estar seguro de que adquirirá el hábito y un día olvidará lanzar un evento en algún lugar donde deba hacerse.
Jason Williams

3
Hay una sugerencia de MS-Connect para los eventos de referencia débiles aquí lo que resolvería este problema, aunque en mi opinión deberíamos reemplazar por completo el modelo de eventos muy pobre, con una acoplados débilmente, como la utilizada por el CAB.
BlueRaja - Danny Pflughoeft

+1 de mi parte, gracias! Bueno, no, gracias por el trabajo de revisión de código que tuve que hacer.
Bob Denny,

@ BlueRaja-DannyPflughoeft Con eventos débiles tiene otro problema: no puede suscribirse lambdas. No se puede escribirtimer.Tick += (s, e,) => { Console.WriteLine(s); }
Ark-kun

@ Ark-kun sí, las lambdas lo hacen aún más difícil, tendrías que guardar tu lambda en una variable y usarla en tu código de desmontaje. Un poco destruye la simplicidad de escribir lambdas, ¿no?
Timothy Walters

63

Tal vez no sea realmente un problema porque el comportamiento está escrito claramente en MSDN, pero me rompió el cuello una vez porque lo encontré bastante contra-intuitivo:

Image image = System.Drawing.Image.FromFile("nice.pic");

Este tipo deja el "nice.pic"archivo bloqueado hasta que se elimine la imagen. En el momento en que lo enfrenté, pensé que sería bueno cargar íconos sobre la marcha y no me di cuenta (al principio) de que terminé con docenas de archivos abiertos y bloqueados. La imagen realiza un seguimiento de dónde había cargado el archivo ...

¿Cómo resolver esto? Pensé que un trazador de líneas haría el trabajo. Esperaba un parámetro adicional para FromFile(), pero no tenía ninguno, así que escribí esto ...

using (Stream fs = new FileStream("nice.pic", FileMode.Open, FileAccess.Read))
{
    image = System.Drawing.Image.FromStream(fs);
}

10
Estoy de acuerdo en que este comportamiento no tiene sentido. No puedo encontrar ninguna explicación que no sea "este comportamiento es por diseño".
MusiGenesis

1
Ah, y lo mejor de esta solución es que si intentas llamar a Image.ToStream (no recuerdo el nombre exacto) luego no funcionará.
Joshua

55
Necesito verificar algún código. Brb.
Esben Skov Pedersen

77
@EsbenSkovPedersen Un comentario tan simple pero divertido y seco. Me alegró el día.
Inisheer

51

Si cuenta ASP.NET, diría que el ciclo de vida de los formularios web es un gran problema para mí. He pasado innumerables horas depurando código de formularios web mal escrito, solo porque muchos desarrolladores simplemente no entienden cuándo usar qué controlador de eventos (incluido yo, lamentablemente).


26
Es por eso que me mudé a MVC ... ver dolores de cabeza de estado ...
chakrit

29
Había otra pregunta completamente dedicada específicamente a las trampas de ASP.NET (merecidamente). El concepto básico de ASP.NET (hacer que las aplicaciones web parezcan aplicaciones de Windows para el desarrollador) es tan terriblemente equivocado que no estoy seguro de que incluso cuente como un "problema".
MusiGenesis

1
MusiGenesis Ojalá pudiera votar tu comentario cien veces.
csauve

3
@MusiGenesis Parece equivocado ahora, pero en ese momento, la gente quería que sus aplicaciones web (las aplicaciones fueran la palabra clave: ASP.NET WebForms no estaba realmente diseñado para alojar un blog) para que se comportaran igual que sus aplicaciones de Windows. Esto solo cambió relativamente recientemente y mucha gente todavía "no está del todo allí". Todo el problema era que la abstracción era demasiado permeable: la web no se comportaba tanto como una aplicación de escritorio, lo que generaba confusión en casi todos.
Luaan

1
Irónicamente, lo primero que vi sobre ASP.NET fue un video de Microsoft que demuestra la facilidad con que puedes crear un sitio de blog usando ASP.NET.
MusiGenesis

51

sobrecargado == operadores y contenedores sin tipo (listas de matrices, conjuntos de datos, etc.):

string my = "my ";
Debug.Assert(my+"string" == "my string"); //true

var a = new ArrayList();
a.Add(my+"string");
a.Add("my string");

// uses ==(object) instead of ==(string)
Debug.Assert(a[1] == "my string"); // true, due to interning magic
Debug.Assert(a[0] == "my string"); // false

Soluciones?

  • siempre use string.Equals(a, b)cuando compare tipos de cadenas

  • usando genéricos como List<string>para asegurar que ambos operandos sean cadenas.


66
Tiene espacios adicionales allí que hacen que todo esté mal, pero si elimina los espacios, la última línea seguirá siendo verdadera ya que "mi" + "cadena" sigue siendo una constante.
Jon Skeet

1
ack! tienes razón :) ok, edité un poco.
Jimmy

Se genera una advertencia sobre dichos usos.
chakrit

11
Sí, uno de los mayores defectos del lenguaje C # es el operador == en la clase Object Deberían habernos obligado a usar ReferenceEquals.
erikkallen

2
Afortunadamente, desde 2.0 hemos tenido genéricos. Hay menos de qué preocuparse si está utilizando List <string> en el ejemplo anterior en lugar de ArrayList. Además, hemos obtenido rendimiento de él, ¡sí! Siempre elimino viejas referencias a ArrayLists en nuestro código heredado.
JoelC

48
[Serializable]
class Hello
{
    readonly object accountsLock = new object();
}

//Do stuff to deserialize Hello with BinaryFormatter
//and now... accountsLock == null ;)

Moraleja de la historia: los inicializadores de campo no se ejecutan al deserializar un objeto


8
Sí, odio la serialización .NET por no ejecutar el constructor predeterminado. Desearía que fuera imposible construir un objeto sin llamar a ningún constructor, pero lamentablemente no lo es.
Roman Starkov

45

DateTime.ToString ("dd / MM / aaaa") ; En realidad, esto no siempre le dará dd / MM / aaaa, sino que tendrá en cuenta la configuración regional y reemplazará su separador de fecha dependiendo de dónde se encuentre. Entonces puede obtener dd-MM-aaaa o algo similar.

La forma correcta de hacer esto es usar DateTime.ToString ("dd '/' MM '/' aaaa");


Se supone que DateTime.ToString ("r") se convierte a RFC1123, que usa GMT. GMT está dentro de una fracción de segundo desde UTC, y sin embargo, el especificador de formato "r" no se convierte a UTC , incluso si el DateTime en cuestión se especifica como Local.

Esto da como resultado el siguiente problema (varía según qué tan lejos esté su hora local de UTC):

DateTime.Parse("Tue, 06 Sep 2011 16:35:12 GMT").ToString("r")
>              "Tue, 06 Sep 2011 17:35:12 GMT"

Whoops!


19
Cambió mm a MM: mm son minutos y MM son meses. Otro problema, supongo ...
Kobi

1
Pude ver cómo esto sería un problema si no lo supieras (yo no) ... pero estoy tratando de averiguar cuándo querrías el comportamiento en el que estás tratando específicamente de imprimir una fecha que no coincide con la configuración regional.
Beska

66
@Beska: Debido a que está escribiendo en un archivo, debe estar en un formato específico, con un formato de fecha específico.
GvS

11
Soy de la opinión de que los valores predeterminados que se localizan son peores que al revés. Al menos, el desarrollador ignoró completamente la localización, el código funciona en máquinas localizadas de manera diferente. De esta manera, el código probablemente no funcione.
Joshua

32
En realidad, creo que la forma correcta de hacerlo seríaDateTime.ToString("dd/MM/yyyy", CultureInfo.InvariantCulture);
BlueRaja - Danny Pflughoeft

44

Vi este publicado el otro día, y creo que es bastante oscuro y doloroso para aquellos que no saben

int x = 0;
x = x++;
return x;

Como eso devolverá 0 y no 1 como la mayoría esperaría


37
Sin embargo, espero que eso realmente no muerda a las personas, ¡realmente espero que no lo escriban en primer lugar! (Es interesante de todos modos, por supuesto.)
Jon Skeet

12
No creo que esto sea muy oscuro ...
Chris Marasti-Georg

10
Al menos, en C #, los resultados están definidos, si son inesperados. En C ++, podría ser 0 o 1, o cualquier otro resultado, incluida la finalización del programa.
James Curran

77
Esto no es una trampa; x = x ++ -> x = x, luego incremente x .... x = ++ x -> incremente x luego x = x
Kevin

28
@ Kevin: No creo que sea tan simple. Si x = x ++ fuera equivalente a x = x seguido de x ++, entonces el resultado sería x = 1. En cambio, creo que lo que sucede es que primero se evalúa la expresión a la derecha del signo igual (dando 0), entonces x es incremental (dando x = 1), y finalmente se realiza la asignación (dando x = 0 una vez más).
Tim Goodman

39

Llego un poco tarde a esta fiesta, pero tengo dos problemas que me han mordido recientemente:

Resolución de fecha y hora

La propiedad Ticks mide el tiempo en 10 millonésimas de segundo (bloques de 100 nanosegundos), sin embargo, la resolución no es de 100 nanosegundos, es de aproximadamente 15 ms.

Este código:

long now = DateTime.Now.Ticks;
for (int i = 0; i < 10; i++)
{
    System.Threading.Thread.Sleep(1);
    Console.WriteLine(DateTime.Now.Ticks - now);
}

le dará una salida de (por ejemplo):

0
0
0
0
0
0
0
156254
156254
156254

Del mismo modo, si observa DateTime.Now.Millisecond, obtendrá valores en fragmentos redondeados de 15.625 ms: 15, 31, 46, etc.

Este comportamiento particular varía de un sistema a otro , pero hay otros problemas relacionados con la resolución en esta API de fecha / hora.


Ruta combinada

Es una excelente manera de combinar rutas de archivos, pero no siempre se comporta de la manera que cabría esperar.

Si el segundo parámetro comienza con un \carácter, no le dará una ruta completa:

Este código:

string prefix1 = "C:\\MyFolder\\MySubFolder";
string prefix2 = "C:\\MyFolder\\MySubFolder\\";
string suffix1 = "log\\";
string suffix2 = "\\log\\";

Console.WriteLine(Path.Combine(prefix1, suffix1));
Console.WriteLine(Path.Combine(prefix1, suffix2));
Console.WriteLine(Path.Combine(prefix2, suffix1));
Console.WriteLine(Path.Combine(prefix2, suffix2));

Te da esta salida:

C:\MyFolder\MySubFolder\log\
\log\
C:\MyFolder\MySubFolder\log\
\log\

17
La cuantificación de los tiempos en intervalos de ~ 15 ms no se debe a una falta de precisión en el mecanismo de temporización subyacente (me olvidé de explicar esto antes). Es porque su aplicación se ejecuta dentro de un sistema operativo multitarea. Windows se registra con su aplicación cada 15 ms aproximadamente, y durante el poco tiempo que le queda, su aplicación procesa todos los mensajes que se pusieron en cola desde su última porción. Todas sus llamadas dentro de ese segmento devuelven exactamente al mismo tiempo porque todas se hacen exactamente al mismo tiempo.
MusiGenesis

2
@MusiGenesis: sé (ahora) cómo funciona, pero me parece engañoso tener una medida tan precisa que en realidad no es tan precisa. Es como decir que sé mi estatura en nanómetros cuando realmente la estoy redondeando a los diez millones más cercanos.
Damovisa

77
DateTime es bastante capaz de almacenar hasta una sola marca; es DateTime. Ahora eso no está usando esa precisión.
Ruben

16
El '\' extra es un problema para muchas personas de Unix / Mac / Linux. En Windows, si hay un '\' inicial, significa que queremos ir a la raíz de la unidad (es decir, C :) probarlo en un CDcomando para ver lo que quiero decir ... 1) Ir a C:\Windows\System322) Tipo CD \Users3) ¡Woah! Ahora estás en C:\Users... ¿Lo tienes ? ... Path.Combine (@ "C: \ Windows \ System32", @ "\ Users") debería volver, lo \Usersque significa precisamente el[current_drive_here]:\Users
chakrit

8
Incluso sin el "sueño", esto funciona de la misma manera. Esto no tiene nada que ver con que la aplicación se programe cada 15 ms. La función nativa llamada por DateTime.UtcNow, GetSystemTimeAsFileTime, parece tener una resolución pobre.
Jimbo

38

Cuando comienzas un proceso (usando System.Diagnostics) que escribe en la consola, pero nunca lees la consola. Fuera de la transmisión, después de una cierta cantidad de salida, tu aplicación parecerá bloquearse.


3
Lo mismo puede suceder cuando redirige stdout y stderr y usa dos llamadas ReadToEnd en secuencia. Para un manejo seguro de stdout y stderr, debe crear un hilo de lectura para cada uno de ellos.
Sebastiaan M

34

No hay atajos de operador en Linq-To-Sql

Ver aquí .

En resumen, dentro de la cláusula condicional de una consulta Linq-To-Sql, no puede usar atajos condicionales como ||y &&para evitar excepciones de referencia nula; ¡Linq-To-Sql evalúa ambos lados del operador OR o AND incluso si la primera condición elimina la necesidad de evaluar la segunda condición!


8
TIL BRB, re-optimizando unos cientos de consultas LINQ ...
tsilb

30

Usar parámetros predeterminados con métodos virtuales

abstract class Base
{
    public virtual void foo(string s = "base") { Console.WriteLine("base " + s); }
}

class Derived : Base
{
    public override void foo(string s = "derived") { Console.WriteLine("derived " + s); }
}

...

Base b = new Derived();
b.foo();

Salida:
base derivada


10
Extraño, pensé que esto es completamente obvio. Si el tipo declarado es Base, ¿de dónde debería obtener el compilador el valor predeterminado Base? Pensé que es un poco más complicado que el valor predeterminado pueda ser diferente si el tipo declarado es el tipo derivado , aunque el método llamado (estáticamente) es el método base.
Timwi

1
¿Por qué una implementación de un método obtiene el valor predeterminado de otra implementación?
staafl

1
@staafl Los argumentos predeterminados se resuelven en tiempo de compilación, no en tiempo de ejecución.
fredoverflow

1
Yo diría que este problema son los parámetros predeterminados en general: las personas a menudo no se dan cuenta de que se resuelven en tiempo de compilación, en lugar de en tiempo de ejecución.
Luaan

44
@FredOverflow, mi pregunta fue conceptual. Aunque el comportamiento tiene sentido en la implementación, no es intuitivo y es probable que sea una fuente de errores. En mi humilde opinión, el compilador de C # no debería permitir cambiar los valores de los parámetros predeterminados al anular.
staafl

27

Valorar objetos en colecciones mutables

struct Point { ... }
List<Point> mypoints = ...;

mypoints[i].x = 10;

no tiene efecto.

mypoints[i]devuelve una copia de un Pointobjeto de valor. C # felizmente le permite modificar un campo de la copia. Silenciosamente no haciendo nada.


Actualización: esto parece solucionarse en C # 3.0:

Cannot modify the return value of 'System.Collections.Generic.List<Foo>.this[int]' because it is not a variable

66
Puedo ver por qué eso es confuso, teniendo en cuenta que de hecho funciona con matrices (contrario a su respuesta), pero no con otras colecciones dinámicas, como List <Point>.
Lasse V. Karlsen

2
Tienes razón. Gracias. Arreglé mi respuesta :). arr[i].attr=es una sintaxis especial para matrices que no puede codificar en contenedores de biblioteca; (. ¿Por qué está permitido (<value expression>). attr = <expr>? ¿Puede tener sentido?
Bjarke Ebert

1
@Bjarke Ebert: Hay algunos casos en los que tendría sentido, pero desafortunadamente no hay forma de que el compilador los identifique y los permita. Escenario de uso de muestra: una estructura inmutable que contiene una referencia a una matriz cuadrada bidimensional junto con un indicador de "rotar / voltear". La estructura en sí sería inmutable, por lo que escribir en un elemento de una instancia de solo lectura debería estar bien, pero el compilador no sabrá que el configurador de propiedades en realidad no va a escribir la estructura, y por lo tanto no lo permitirá .
supercat

25

Quizás no sea lo peor, pero algunas partes del marco .net usan grados, mientras que otras usan radianes (y la documentación que aparece con Intellisense nunca te dice cuál, debes visitar MSDN para averiguarlo)

Todo esto podría haberse evitado teniendo una Angleclase en su lugar ...


Me sorprende este tiene tantos upvotes, teniendo en cuenta mis otras trampas son significativamente peor que esto
BlueRaja - Danny Pflughoeft

22

Para los programadores de C / C ++, la transición a C # es natural. Sin embargo, el mayor problema con el que me he encontrado personalmente (y he visto con otros que hacen la misma transición) no es comprender completamente la diferencia entre clases y estructuras en C #.

En C ++, las clases y las estructuras son idénticas; solo difieren en la visibilidad predeterminada, donde las clases usan la visibilidad privada y las estructuras usan la visibilidad pública. En C ++, esta definición de clase

    class A
    {
    public:
        int i;
    };

es funcionalmente equivalente a esta definición de estructura.

    struct A
    {
        int i;
    };

Sin embargo, en C #, las clases son tipos de referencia, mientras que las estructuras son tipos de valor. Esto hace una GRAN diferencia en (1) decidir cuándo usar uno sobre el otro, (2) probar la igualdad de objetos, (3) el rendimiento (por ejemplo, boxing / unboxing), etc.

Hay todo tipo de información en la web relacionada con las diferencias entre los dos (por ejemplo, aquí ). Recomiendo encarecidamente que cualquiera que haga la transición a C # tenga al menos un conocimiento práctico de las diferencias y sus implicaciones.


13
Entonces, ¿el peor problema es que las personas no se molestan en tomarse el tiempo para aprender el idioma antes de usarlo?
BlueRaja - Danny Pflughoeft

3
@ BlueRaja-DannyPflughoeft Más como el clásico truco de lenguajes aparentemente similares: usan palabras clave muy similares y, en muchos casos, sintaxis, pero funcionan de manera muy diferente.
Luaan

19

Recolección de basura y desechar (). Aunque no tiene que hacer nada para liberar memoria , aún tiene que liberar recursos a través de Dispose (). Esto es inmensamente fácil de olvidar cuando usa WinForms o rastrea objetos de cualquier manera.


2
El bloque using () resuelve perfectamente este problema. Cada vez que vea una llamada para Eliminar, puede refactorizar de forma inmediata y segura para usar usando ().
Jeremy Frey

55
Creo que la preocupación era implementar IDisposable correctamente.
Mark Brackett

44
Por otro lado, el hábito de usar () puede morderte inesperadamente, como cuando trabajas con PInvoke. No desea deshacerse de algo a lo que la API todavía hace referencia.
MusiGenesis

3
Implementar IDisposable correctamente es muy difícil de entender, incluso el mejor consejo que he encontrado sobre esto (.NET Framework Guidelines) puede ser confuso de aplicar hasta que finalmente "lo entiendas".
Molesto

1
El mejor consejo que he encontrado sobre IDisposable proviene de Stephen Cleary, que incluye tres reglas fáciles y un artículo en profundidad sobre IDisposable
Roman Starkov,

19

Implementar matrices IList

Pero no lo implementes. Cuando llamas a Agregar, te dice que no funciona. Entonces, ¿por qué una clase implementa una interfaz cuando no puede soportarla?

Compila, pero no funciona:

IList<int> myList = new int[] { 1, 2, 4 };
myList.Add(5);

Tenemos mucho este problema, porque el serializador (WCF) convierte todas las ILists en matrices y obtenemos errores de tiempo de ejecución.


8
En mi humilde opinión, el problema es que Microsoft no tiene suficientes interfaces definidas para las colecciones. En mi humilde opinión, debería tener iEnumerable, iMultipassEnumerable (admite Reset y garantiza que coincidan varios pases), iLiveEnumerable (tendría una semántica parcialmente definida si la colección cambia durante la enumeración; los cambios pueden o no aparecer en la enumeración, pero no deberían causar resultados falsos o excepciones), iReadIndexable, iReadWriteIndexable, etc. Debido a que las interfaces pueden "heredar" otras interfaces, esto no habría agregado mucho trabajo adicional, si lo hubiera (ahorraría los stubs NotImplemented).
supercat

@supercat, eso sería confuso para los principiantes y ciertos codificadores de mucho tiempo. Creo que las colecciones .NET y sus interfaces son maravillosamente elegantes. Pero aprecio tu humildad. ;)
Jordania

@ Jordania: desde que escribí lo anterior, he decidido que un mejor enfoque habría sido tener ambos IEnumerable<T>y IEnumerator<T>admitir una Featurespropiedad, así como algunos métodos "opcionales" cuya utilidad estaría determinada por lo que reportaron las "Características". Sin embargo, mantengo mi punto principal, que es que hay casos en los que el código que recibe un IEnumerable<T>necesitará promesas más fuertes que las que IEnumerable<T>ofrece. Llamar ToListproduciría una IEnumerable<T>que cumpla tales promesas, pero en muchos casos sería innecesariamente costosa. Yo diría que debería haber ...
supercat

... un medio por el cual el código que recibe IEnumerable<T>podría hacer una copia de los contenidos si fuera necesario, pero podría abstenerse de hacerlo innecesariamente.
supercat

Su opción no es absolutamente legible. Cuando veo una IList en el código, sé con qué estoy trabajando en lugar de tener que probar una propiedad de Features. A los programadores les gusta olvidar que una característica importante del código es que puede ser leído por personas, no solo por computadoras. El espacio de nombres de colecciones .NET no es ideal, pero es bueno, y a veces encontrar la mejor solución no es una cuestión de ajustar un principio de manera más ideal. Algunos de los peores códigos con los que he trabajado fueron códigos que intentaron encajar DRY de manera ideal. Lo deseché y lo reescribí. Era solo un mal código. No me gustaría usar su marco en absoluto.
Jordan

18

foreach bucles variables alcance!

var l = new List<Func<string>>();
var strings = new[] { "Lorem" , "ipsum", "dolor", "sit", "amet" };
foreach (var s in strings)
{
    l.Add(() => s);
}

foreach (var a in l)
    Console.WriteLine(a());

imprime cinco "amet", mientras que el siguiente ejemplo funciona bien

var l = new List<Func<string>>();
var strings = new[] { "Lorem" , "ipsum", "dolor", "sit", "amet" };
foreach (var s in strings)
{
    var t = s;
    l.Add(() => t);
}

foreach (var a in l)
    Console.WriteLine(a());

11
Esto es esencialmente equivalente al ejemplo de Jon con métodos anónimos.
Mehrdad Afshari

3
Excepto que es aún más confuso con foreach donde la variable "s" es más fácil de mezclar con la variable de ámbito. Con los bucles for comunes, la variable de índice claramente es la misma para cada iteración.
Mikko Rantanen

2
blogs.msdn.com/ericlippert/archive/2009/11/12/… y sí, desearía que la variable tuviera un alcance "adecuado".
Roman Starkov


Esencialmente solo está imprimiendo la misma variable una y otra vez sin cambiarla.
Jordan

18

MS SQL Server no puede manejar fechas anteriores a 1753. Significativamente, eso no está sincronizado con la DateTime.MinDateconstante .NET , que es 1/1/1. Entonces, si intenta guardar una fecha mínima, una fecha con formato incorrecto (como me sucedió recientemente en una importación de datos) o simplemente la fecha de nacimiento de William the Conqueror, tendrá problemas. No hay una solución integrada para esto; Si es probable que necesite trabajar con fechas anteriores a 1753, debe escribir su propia solución.


17
Francamente, creo que MS SQL Server tiene este derecho y .Net está equivocado. Si hace la investigación, entonces sabe que las fechas anteriores a 1751 se vuelven extravagantes debido a cambios en el calendario, días completamente omitidos, etc. La mayoría de los RDBM tienen algún punto de corte. Esto debería darle un punto de partida: ancestry.com/learn/library/article.aspx?article=3358
NotMe

11
Además, la fecha es 1753 .. Que fue más o menos la primera vez que tenemos un calendario continuo sin que se omitan fechas. SQL 2008 introdujo el tipo de fecha Date y datetime2 que puede aceptar fechas del 1/1/01 al 31/12/9999. Sin embargo, las comparaciones de fechas que usan esos tipos deben considerarse con sospecha si realmente está comparando fechas anteriores a 1753.
NotMe

Oh, cierto, 1753, corregido, gracias.
Shaul Behr

¿Realmente tiene sentido hacer comparaciones de fechas con tales fechas? Quiero decir, para History Channel esto tiene mucho sentido, pero no me veo con ganas de saber el día exacto de la semana en que se descubrió América.
Camilo Martin

55
A través de Wikipedia en Julian Day puede encontrar un programa básico de 13 líneas CALJD.BAS publicado en 1984 que puede hacer cálculos de fecha hasta aproximadamente 5000 AC, teniendo en cuenta los días bisiestos y los días omitidos en 1753. Por lo tanto, no veo por qué "moderno "Los sistemas como SQL2008 deberían hacerlo peor. Es posible que no esté interesado en una representación correcta de la fecha en el siglo XV, pero otros sí, y nuestro software debe manejar esto sin errores. Otro problema son los segundos bisiestos. . .
Roland

18

The Nasty Linq Caching Gotcha

Vea mi pregunta que condujo a este descubrimiento, y al blogger que descubrió el problema.

En resumen, el DataContext mantiene un caché de todos los objetos Linq-to-Sql que alguna vez haya cargado. Si alguien más realiza algún cambio en un registro que haya cargado previamente, no podrá obtener los datos más recientes, ¡ incluso si vuelve a cargar el registro explícitamente!

Esto se debe a una propiedad llamada ObjectTrackingEnableden el DataContext, que por defecto es verdadera. Si establece esa propiedad en falso, el registro se cargará de nuevo cada vez ... PERO ... no puede persistir ningún cambio en ese registro con SubmitChanges ().

GOTCHA!


Iv acaba de pasar un día y medio (¡y mucho pelo!) Persiguiendo este ERROR ...
Quirúrgico

Esto se llama un conflicto de concurrencia y todavía es un problema hoy en día, aunque hay ciertas formas de evitarlo ahora, aunque tienden a ser un poco pesados. DataContext fue una pesadilla. O_o
Jordania

17

El contrato en Stream.Read es algo que he visto tropezar con mucha gente:

// Read 8 bytes and turn them into a ulong
byte[] data = new byte[8];
stream.Read(data, 0, 8); // <-- WRONG!
ulong data = BitConverter.ToUInt64(data);

La razón por la que esto es incorrecto es que Stream.Readleerá como máximo el número especificado de bytes, pero es completamente libre de leer solo 1 byte, incluso si hay otros 7 bytes disponibles antes del final de la secuencia.

No ayuda que esto se parezca tanto a Stream.Write que se garantiza que ha escrito todos los bytes si regresa sin excepción. Tampoco ayuda que el código anterior funcione casi todo el tiempo . Y, por supuesto, no ayuda que no haya un método conveniente y listo para leer exactamente N bytes correctamente.

Entonces, para tapar el agujero y aumentar la conciencia de esto, aquí hay un ejemplo de una forma correcta de hacer esto:

    /// <summary>
    /// Attempts to fill the buffer with the specified number of bytes from the
    /// stream. If there are fewer bytes left in the stream than requested then
    /// all available bytes will be read into the buffer.
    /// </summary>
    /// <param name="stream">Stream to read from.</param>
    /// <param name="buffer">Buffer to write the bytes to.</param>
    /// <param name="offset">Offset at which to write the first byte read from
    ///                      the stream.</param>
    /// <param name="length">Number of bytes to read from the stream.</param>
    /// <returns>Number of bytes read from the stream into buffer. This may be
    ///          less than requested, but only if the stream ended before the
    ///          required number of bytes were read.</returns>
    public static int FillBuffer(this Stream stream,
                                 byte[] buffer, int offset, int length)
    {
        int totalRead = 0;
        while (length > 0)
        {
            var read = stream.Read(buffer, offset, length);
            if (read == 0)
                return totalRead;
            offset += read;
            length -= read;
            totalRead += read;
        }
        return totalRead;
    }

    /// <summary>
    /// Attempts to read the specified number of bytes from the stream. If
    /// there are fewer bytes left before the end of the stream, a shorter
    /// (possibly empty) array is returned.
    /// </summary>
    /// <param name="stream">Stream to read from.</param>
    /// <param name="length">Number of bytes to read from the stream.</param>
    public static byte[] Read(this Stream stream, int length)
    {
        byte[] buf = new byte[length];
        int read = stream.FillBuffer(buf, 0, length);
        if (read < length)
            Array.Resize(ref buf, read);
        return buf;
    }

1
O, en su ejemplo explícito: var r = new BinaryReader(stream); ulong data = r.ReadUInt64();. BinaryReader también tiene un FillBuffermétodo ...
jimbobmcgee

15

Eventos

Nunca entendí por qué los eventos son una característica del lenguaje. Son complicados de usar: debe verificar si hay nulos antes de llamar, debe anular el registro (usted mismo), no puede averiguar quién está registrado (por ejemplo: ¿me registré?). ¿Por qué un evento no es solo una clase en la biblioteca? Básicamente un especializado List<delegate>?


1
Además, el subprocesamiento múltiple es doloroso. Todos estos problemas, excepto los nulos, se corrigen en CAB (cuyas características realmente deberían integrarse en el lenguaje): los eventos se declaran globalmente y cualquier método puede declararse a sí mismo como un "suscriptor" de cualquier evento. Mi único problema con CAB es que los nombres de eventos globales son cadenas en lugar de enumeraciones (que podrían solucionarse con enumeraciones más inteligentes, como Java, que funcionan inherentemente como cadenas) . CAB es difícil de configurar, pero hay un clon simple de código abierto disponible aquí .
BlueRaja - Danny Pflughoeft

3
No me gusta la implementación de eventos .net. La suscripción al evento debe manejarse llamando a un método que agrega la suscripción y devuelve un ID desechable que, cuando se elimine, eliminará la suscripción. No hay necesidad de una construcción especial que combine un método "agregar" y "eliminar" cuya semántica puede ser algo dudosa, especialmente si uno intenta agregar y luego eliminar un delegado de multidifusión (por ejemplo, Agregar "B" seguido de "AB", luego eliminar "B" (dejando "BA") y "AB" (todavía dejando "BA"). Ups.
supercat

@supercat ¿Cómo reescribirías button.Click += (s, e) => { Console.WriteLine(s); }?
Ark-kun

Si tuviera que poder darme de baja por separado de otros eventos, IEventSubscription clickSubscription = button.SubscribeClick((s,e)=>{Console.WriteLine(s);});y darme de baja a través de clickSubscription.Dispose();. Si mi objeto mantendría todas las suscripciones durante toda su vida útil, MySubscriptions.Add(button.SubscribeClick((s,e)=>{Console.WriteLine(s);}));y luego MySubscriptions.Dispose()eliminaría todas las suscripciones.
supercat

@ Ark-kun: Tener que mantener los objetos que encapsulan las suscripciones externas puede parecer una molestia, pero considerar las suscripciones como entidades permitiría agregarlas con un tipo que pueda garantizar que se limpien, algo que de otra manera es muy difícil.
supercat

14

Hoy arreglé un error que eludió por mucho tiempo. El error estaba en una clase genérica que se usaba en un escenario de subprocesos múltiples y se usaba un campo int estático para proporcionar sincronización sin bloqueo usando Interlocked. El error fue causado porque cada instanciación de la clase genérica para un tipo tiene su propia estática. Entonces, cada subproceso tiene su propio campo estático y no se usó un bloqueo como se esperaba.

class SomeGeneric<T>
{
    public static int i = 0;
}

class Test
{
    public static void main(string[] args)
    {
        SomeGeneric<int>.i = 5;
        SomeGeneric<string>.i = 10;
        Console.WriteLine(SomeGeneric<int>.i);
        Console.WriteLine(SomeGeneric<string>.i);
        Console.WriteLine(SomeGeneric<int>.i);
    }
}

Esto imprime 5 10 5


55
puede tener una clase base no genérica, que defina las estadísticas y herede los genéricos de ella. Aunque nunca me enamoré de este comportamiento en C #, todavía recuerdo las largas horas de depuración de algunas plantillas de C ++ ... ¡Eww! :)
Paulius

77
Extraño, pensé que esto era obvio. Solo piense en lo que debería hacer si ituviera el tipo T.
Timwi

1
El parámetro tipo es parte de Type. SomeGeneric<int>es un tipo diferente de SomeGeneric<string>; así que, por supuesto, cada uno tiene su propiopublic static int i
radarbob

13

Los enumerables se pueden evaluar más de una vez

Te morderá cuando tengas un enumerable vagamente enumerado y lo repitas dos veces y obtengas resultados diferentes. (u obtiene los mismos resultados pero se ejecuta dos veces innecesariamente)

Por ejemplo, mientras escribía una prueba determinada, necesitaba algunos archivos temporales para probar la lógica:

var files = Enumerable.Range(0, 5)
    .Select(i => Path.GetTempFileName());

foreach (var file in files)
    File.WriteAllText(file, "HELLO WORLD!");

/* ... many lines of codes later ... */

foreach (var file in files)
    File.Delete(file);

Imagina mi sorpresa cuando File.Delete(file)lanza FileNotFound!!

Lo que sucede aquí es que el filesenumerable se repitió dos veces (los resultados de la primera iteración simplemente no se recuerdan) y en cada nueva iteración volvería a llamar Path.GetTempFilename()para que obtenga un conjunto diferente de nombres de archivos temporales.

La solución es, por supuesto, enumerar ansiosamente el valor usando ToArray()o ToList():

var files = Enumerable.Range(0, 5)
    .Select(i => Path.GetTempFileName())
    .ToArray();

Esto es aún más aterrador cuando haces algo con varios subprocesos, como:

foreach (var file in files)
    content = content + File.ReadAllText(file);

y descubres que content.Lengthsigue siendo 0 después de todas las escrituras !! Luego, comienzas a comprobar rigurosamente que no tienes una condición de carrera cuando ... después de una hora desperdiciada ... descubriste que es solo esa pequeña cosa de Enumerable que olvidaste ...


Esto es por diseño. Se llama ejecución diferida. Entre otras cosas, está destinado a simular construcciones TSQL. Cada vez que selecciona de una vista sql obtiene resultados diferentes. También permite el encadenamiento, lo que es útil para los almacenes de datos remotos, como SQL Server. De lo contrario, x.Select.Where.OrderBy enviaría 3 comandos separados a la base de datos ...
as9876

@AYS ¿Te perdiste la palabra "Gotcha" en el título de la pregunta?
chakrit

Pensé que gotcha significaba una supervisión de los diseñadores, no algo intencional.
as9876

Tal vez debería haber otro tipo de IEnumerables no reiniciables. Me gusta, AutoBufferedEnumerable? Uno podría implementarlo fácilmente. Esto parece deberse principalmente a la falta de conocimiento del programador, no creo que haya nada malo con el comportamiento actual.
Eldritch Conundrum

13

Acabo de encontrar uno extraño que me tuvo atrapado en la depuración por un tiempo:

Puede incrementar nulo para un int anulable sin lanzar una excepción y el valor permanece nulo.

int? i = null;
i++; // I would have expected an exception but runs fine and stays as null

Ese es el resultado de cómo C # aprovecha las operaciones para tipos anulables. Es un poco similar a NaN que consume todo lo que le arrojas.
IllidanS4 quiere que Monica regrese

10
TextInfo textInfo = Thread.CurrentThread.CurrentCulture.TextInfo;

textInfo.ToTitleCase("hello world!"); //Returns "Hello World!"
textInfo.ToTitleCase("hElLo WoRld!"); //Returns "Hello World!"
textInfo.ToTitleCase("Hello World!"); //Returns "Hello World!"
textInfo.ToTitleCase("HELLO WORLD!"); //Returns "HELLO WORLD!"

Sí, este comportamiento está documentado, pero eso ciertamente no lo hace correcto.


55
No estoy de acuerdo: cuando una palabra está en mayúsculas, puede tener un significado especial de que no desea meterse con el Título del caso, por ejemplo, "presidente de los Estados Unidos" -> "Presidente de los Estados Unidos", no "Presidente de los Estados Unidos". Estados Unidos".
Shaul Behr

55
@Shaul: En cuyo caso, se debe especificar esto como un parámetro para evitar confusiones, porque nunca he conocido a nadie que espera que este comportamiento antes de tiempo - lo que hace de este un Gotcha !
BlueRaja - Danny Pflughoeft 05 de
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.