ReSharper alerta: “Campo estático no 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;
    }
}

Isso está errado? Eu diria que isso realmente tem um static readonlycampo para cada um dos possíveis EnumRouteConstraint<T>que eu ocorra.

bevacqua
fonte
Às vezes é uma característica, às vezes um aborrecimento. Eu desejei C # tinha alguma palavra-chave para distingui-los
Nawfal

Respostas:

468

Não há problema em ter um campo estático em um tipo genérico, desde que você saiba que realmente obterá um campo por combinação de argumentos de tipo. Meu palpite é que o R # está apenas avisando caso você não esteja ciente disso.

Aqui está um exemplo disso:

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 você pode ver, Generic<string>.Fooé um campo diferente de Generic<object>.Foo- eles mantêm valores separados.

Jon Skeet
fonte
Isso também é verdade quando classes genéricas herdam de uma classe não genérica que contém tipos estáticos. Por exemplo, se eu criar class BaseFoocontendo um membro estático, daí derivar class Foo<T>: BaseFootodas as Foo<T>classes compartilharão o mesmo valor de membro estático?
precisa saber é o seguinte
2
Respondendo ao meu próprio comentário aqui, mas sim, todos os Foo <T> terão o mesmo valor estático se estiverem contidos em uma classe base não genérica. Veja também dotnetfiddle.net/Wz75ya
bikeman868
147

No wiki do JetBrains :

Na grande maioria dos casos, ter um campo estático em um tipo genérico é sinal de erro. A razão para isso é que um campo estático em um tipo genérico não será compartilhado entre instâncias de diferentes tipos construídos próximos. Isso significa que, para uma classe genérica C<T>que possui um campo estático X, os valores de C<int>.Xe C<string>.X têm valores independentes completamente diferentes.

Nos raros casos em que você não precisa os campos estáticos 'especializada', sinta-se livre para suprimir o aviso.

Se você precisar ter um campo estático compartilhado entre instâncias com diferentes argumentos genéricos, defina uma classe base não genérica para armazenar seus membros estáticos e defina seu tipo genérico para herdar desse tipo.

AakashM
fonte
13
Ao empregar um tipo genérico, tecnicamente você acaba com uma classe distinta e separada para cada tipo genérico que está hospedando. Ao declarar duas classes separadas e não genéricas, você não espera compartilhar variáveis ​​estáticas entre elas; portanto, por que os genéricos devem ser diferentes? A única maneira de isso ser considerado raro é se a maioria dos desenvolvedores não entender o que estão fazendo ao criar classes genéricas.
21414 Syndog
2
@ Syndog o comportamento descrito da estática dentro de uma classe genérica parece bom e compreensível para mim. Mas acho que a razão por trás desses avisos é que nem toda equipe possui apenas desenvolvedores experientes e focados. O código correto se torna propenso a erros devido à qualificação do desenvolvedor.
perfil completo de Stas Ivanov
Mas e se eu não quiser criar uma classe base não genérica apenas para conter esses campos estáticos. Posso apenas suprimir os avisos, neste caso?
Tom Lint
@ TomLint, se você souber o que está fazendo, suprimir os avisos é realmente a coisa a fazer.
AakashM 27/11/18
65

Isso não é necessariamente um erro - está avisando sobre um possível mal-entendido dos genéricos de C #.

A maneira mais fácil de lembrar o que os genéricos fazem é o seguinte: Genéricos são "blueprints" para criar classes, assim como as classes são "blueprints" para criar objetos. (Bem, isso é uma simplificação. Você também pode usar genéricos de métodos.)

Desse ponto de vista, MyClassRecipe<T>não é uma classe - é uma receita, um plano, da aparência da sua classe. Depois de substituir T por algo concreto, por exemplo, int, string etc., você obtém uma classe. É perfeitamente legal ter um membro estático (campo, propriedade, método) declarado em sua classe recém-criada (como em qualquer outra classe) e nenhum sinal de erro aqui. Seria um pouco suspeito, à primeira vista, se você declarar static MyStaticProperty<T> Property { get; set; }dentro do plano da sua classe, mas isso também é legal. Sua propriedade também seria parametrizada ou modelada.

Não é de admirar que as estatísticas de VB sejam chamadas shared. Nesse caso, no entanto, você deve estar ciente de que esses membros "compartilhados" são compartilhados apenas entre instâncias da mesma classe exata e não entre as classes distintas produzidas pela substituição <T>por outra coisa.

Alexander Christov
fonte
1
Eu acho que o nome C ++ torna mais claro de todos. Em C ++, eles são chamados de modelos, que são o que são, modelos para classes concretas.
Michael Brown
8

Já existem várias boas respostas que explicam o aviso e o motivo. Vários desses afirmam algo como ter um campo estático em um tipo genérico geralmente um erro .

Pensei em acrescentar um exemplo de como esse recurso pode ser útil, ou seja, um caso em que suprimir o aviso R # faz sentido.

Imagine que você tem um conjunto de classes de entidade que deseja serializar, digamos em Xml. Você pode criar um serializador para isso usando new XmlSerializerFactory().CreateSerializer(typeof(SomeClass)), mas precisará criar um serializador separado para cada tipo. Usando genéricos, você pode substituir isso pelo seguinte, que pode ser inserido em uma classe genérica da qual as entidades podem derivar:

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

Como você provavelmente não deseja gerar um novo serializador sempre que precisar serializar uma instância de um tipo específico, você pode adicionar o seguinte:

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);
     }
}

Se essa classe NÃO fosse genérica, cada instância da classe usaria a mesma _typeSpecificSerializer.

Porém, como é genérico, um conjunto de instâncias do mesmo tipo Tcompartilhará uma única instância de _typeSpecificSerializer(que será criada para esse tipo específico), enquanto instâncias com um tipo diferente de Tusarão instâncias diferentes de _typeSpecificSerializer.

Um exemplo

Forneceu as duas classes que se estendem 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; }
}

... vamos usá-los:

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();

Nesse caso, sob o capô, firstInste secondInsthaverá instâncias da mesma classe (a saber SerializableEntity<MyFirstEntity>) e, como tal, eles compartilharão uma instância de _typeSpecificSerializer.

thirdInste fourthInstsão exemplos de uma classe diferente ( SerializableEntity<OtherEntity>), e por isso irão partilhar um exemplo de _typeSpecificSerializerque é diferente das outras duas.

Isso significa que você obter diferentes serializador-instances para cada um de seus entidade tipos , mantendo-los estáticos dentro do contexto de cada tipo real (ou seja, compartilhada entre os casos que são de um tipo específico).

Kjartan
fonte
Por causa das regras de inicialização estática (o inicializador estático não é chamado até a classe ser referenciada pela primeira vez), você pode renunciar à verificação no Getter e apenas inicializá-la na declaração de instância estática.
22718 Michael Michael