ReSharper advierte: "Campo estático en tipo genérico"

261
public class EnumRouteConstraint<T> : IRouteConstraint
    where T : struct
{
    private static readonly Lazy<HashSet<string>> _enumNames; // <--

    static EnumRouteConstraint()
    {
        if (!typeof(T).IsEnum)
        {
            throw new ArgumentException(
                Resources.Error.EnumRouteConstraint.FormatWith(typeof(T).FullName));
        }

        string[] names = Enum.GetNames(typeof(T));
        _enumNames = new Lazy<HashSet<string>>(() => new HashSet<string>
        (
            names.Select(name => name), StringComparer.InvariantCultureIgnoreCase
        ));
    }

    public bool Match(HttpContextBase httpContext, Route route, 
                        string parameterName, RouteValueDictionary values, 
                        RouteDirection routeDirection)
    {
        bool match = _enumNames.Value.Contains(values[parameterName].ToString());
        return match;
    }
}

¿Esto esta mal? Supongo que esto realmente tiene un static readonlycampo para cada una de las posibles EnumRouteConstraint<T>que ocurra.

bevacqua
fuente
A veces es una característica, a veces una molestia. Desearía que C # tuviera alguna palabra clave para distinguirlos
nawfal

Respuestas:

468

Está bien tener un campo estático en un tipo genérico, siempre que sepa que realmente obtendrá un campo por combinación de argumentos de tipo. Supongo que R # solo te advierte en caso de que no lo supieras.

Aquí hay un ejemplo de eso:

using System;

public class Generic<T>
{
    // Of course we wouldn't normally have public fields, but...
    public static int Foo;
}

public class Test
{
    public static void Main()
    {
        Generic<string>.Foo = 20;
        Generic<object>.Foo = 10;
        Console.WriteLine(Generic<string>.Foo); // 20
    }
}

Como puede ver, Generic<string>.Fooes un campo diferente de Generic<object>.Foo: contienen valores separados.

Jon Skeet
fuente
¿Es esto también cierto cuando las clases genéricas heredan de una clase no genérica que contiene tipos estáticos? Por ejemplo, si creo que class BaseFoocontiene un miembro estático, entonces derivaré de él class Foo<T>: BaseFoo¿ Foo<T>compartirán todas las clases el mismo valor de miembro estático?
bikeman868
2
Respondiendo mi propio comentario aquí, pero sí, todos los Foo <T> tendrán el mismo valor estático si está contenido en una clase base no genérica. Ver dotnetfiddle.net/Wz75ya
bikeman868
147

De la wiki de JetBrains :

En la gran mayoría de los casos, tener un campo estático en un tipo genérico es un signo de error. La razón de esto es que un campo estático en un tipo genérico no se compartirá entre instancias de diferentes tipos construidos cercanos. Esto significa que para una clase genérica C<T>que tiene un campo estático X, los valores de C<int>.Xy C<string>.X tienen valores independientes completamente diferentes.

En los raros casos en que no necesita los campos estáticos 'especializada', no dude en suprimir la advertencia.

Si necesita tener un campo estático compartido entre instancias con diferentes argumentos genéricos, defina una clase base no genérica para almacenar sus miembros estáticos, luego configure su tipo genérico para heredar de este tipo.

AakashM
fuente
13
Cuando empleas un tipo genérico, técnicamente terminas con una clase distinta y separada para cada tipo genérico que estás alojando. Al declarar dos clases separadas, no genéricas, no esperaría compartir variables estáticas entre ellas, entonces, ¿por qué los genéricos deberían ser diferentes? La única forma en que esto podría considerarse raro es si la mayoría de los desarrolladores no entienden lo que hacen al crear clases genéricas.
Syndog
2
@Syndog, el comportamiento descrito de las estadísticas dentro de una clase genérica se ve bien y es comprensible para mí. Pero supongo que la razón detrás de esas advertencias es que no todos los equipos solo tienen desarrolladores experimentados y enfocados. El código correcto se vuelve propenso a errores debido a la calificación del desarrollador.
Stas Ivanov
Pero, ¿qué pasa si no quiero hacer una clase base no genérica solo para contener estos campos estáticos? ¿Puedo suprimir las advertencias, en este caso?
Tom Lint
@TomLint si sabes lo que estás haciendo, entonces suprimir las advertencias es lo que debes hacer.
AakashM
65

Esto no es necesariamente un error: le está advirtiendo sobre un posible malentendido de los genéricos de C #.

La forma más fácil de recordar lo que hacen los genéricos es la siguiente: los genéricos son "planos" para crear clases, al igual que las clases son "planos" para crear objetos. (Bueno, esta es una simplificación, sin embargo. También puede usar métodos genéricos).

Desde este punto de vista, MyClassRecipe<T>no es una clase, es una receta, un plano, de cómo se vería su clase. Una vez que sustituye T con algo concreto, digamos int, string, etc., obtiene una clase. Es perfectamente legal tener un miembro estático (campo, propiedad, método) declarado en su clase recién creada (como en cualquier otra clase) y no hay signos de error aquí. Sería algo sospechoso, a primera vista, si declara static MyStaticProperty<T> Property { get; set; }dentro de su plan de clase, pero esto también es legal. Su propiedad también estaría parametrizada o con plantilla.

No es de extrañar que en VB se llamen las estadísticas shared. Sin embargo, en este caso, debe tener en cuenta que dichos miembros "compartidos" solo se comparten entre instancias de la misma clase exacta, y no entre las distintas clases producidas al sustituir <T>por otra cosa.

Alexander Christov
fuente
1
Creo que el nombre de C ++ lo hace más claro de todos. En C ++ se llaman Plantillas, que es lo que son, Plantillas para clases concretas.
Michael Brown
8

Aquí hay varias buenas respuestas que explican la advertencia y el motivo. Varios de estos estados, como tener un campo estático en un tipo genérico, generalmente son un error .

Pensé que agregaría un ejemplo de cómo esta característica puede ser útil, es decir, un caso en el que suprimir la advertencia R # tiene sentido.

Imagine que tiene un conjunto de clases de entidad que desea serializar, digamos a Xml. Puede crear un serializador para este uso new XmlSerializerFactory().CreateSerializer(typeof(SomeClass)), pero luego tendrá que crear un serializador separado para cada tipo. Usando genéricos, puede reemplazar eso con lo siguiente, que puede colocar en una clase genérica de la cual las entidades pueden derivar:

new XmlSerializerFactory().CreateSerializer(typeof(T))

Dado que probablemente no desee generar un nuevo serializador cada vez que necesite serializar una instancia de un tipo en particular, puede agregar esto:

public class SerializableEntity<T>
{
    // ReSharper disable once StaticMemberInGenericType
    private static XmlSerializer _typeSpecificSerializer;

    private static XmlSerializer TypeSpecificSerializer
    {
        get
        {
            // Only create an instance the first time. In practice, 
            // that will mean once for each variation of T that is used,
            // as each will cause a new class to be created.
            if ((_typeSpecificSerializer == null))
            {
                _typeSpecificSerializer = 
                    new XmlSerializerFactory().CreateSerializer(typeof(T));
            }

            return _typeSpecificSerializer;
        }
    }

    public virtual string Serialize()
    {
        // .... prepare for serializing...

        // Access _typeSpecificSerializer via the property, 
        // and call the Serialize method, which depends on 
        // the specific type T of "this":
        TypeSpecificSerializer.Serialize(xmlWriter, this);
     }
}

Si esta clase NO fuera genérica, entonces cada instancia de la clase usaría lo mismo _typeSpecificSerializer.

Sin embargo, dado que ES genérico, un conjunto de instancias con el mismo tipo para Tcompartirá una única instancia de _typeSpecificSerializer(que se habrá creado para ese tipo específico), mientras que las instancias con un tipo diferente para Tutilizarán diferentes instancias de _typeSpecificSerializer.

Un ejemplo

Proporcionaron las dos clases que se extienden SerializableEntity<T>:

// Note that T is MyFirstEntity
public class MyFirstEntity : SerializableEntity<MyFirstEntity>
{
    public string SomeValue { get; set; }
}

// Note that T is OtherEntity
public class OtherEntity : SerializableEntity<OtherEntity >
{
    public int OtherValue { get; set; }
}

... usémoslos:

var firstInst = new MyFirstEntity{ SomeValue = "Foo" };
var secondInst = new MyFirstEntity{ SomeValue = "Bar" };

var thirdInst = new OtherEntity { OtherValue = 123 };
var fourthInst = new OtherEntity { OtherValue = 456 };

var xmlData1 = firstInst.Serialize();
var xmlData2 = secondInst.Serialize();
var xmlData3 = thirdInst.Serialize();
var xmlData4 = fourthInst.Serialize();

En este caso, bajo el capó, firstInsty secondInstserán instancias de la misma clase (a saber SerializableEntity<MyFirstEntity>), y como tal, compartirán una instancia de _typeSpecificSerializer.

thirdInsty fourthInstson instancias de una clase diferente ( SerializableEntity<OtherEntity>), y así se comparten una instancia de _typeSpecificSerializerque es diferente de los otros dos.

Esto significa que obtiene diferentes instancias de serializador para cada uno de sus tipos de entidad , mientras las mantiene estáticas dentro del contexto de cada tipo real (es decir, compartidas entre instancias que son de un tipo específico).

Kjartan
fuente
Debido a las reglas para la inicialización estática (el inicializador estático no se llama hasta que se hace referencia a la clase por primera vez), puede renunciar a la verificación en el Getter y simplemente inicializarlo en la declaración de instancia estática.
Michael Brown