ReSharper warns: “Static field in generic type”

2019-01-21 12:25发布

问题:

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

Is this wrong? I would assume that this actually has a static readonly field for each of the possible EnumRouteConstraint<T> that I happen to instance.

回答1:

It's fine to have a static field in a generic type, so long as you know that you'll really get one field per combination of type arguments. My guess is that R# is just warning you in case you weren't aware of that.

Here's an example of that:

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

As you can see, Generic<string>.Foo is a different field from Generic<object>.Foo - they hold separate values.



回答2:

From the JetBrains wiki:

In the vast majority of cases, having a static field in a generic type is a sign of an error. The reason for this is that a static field in a generic type will not be shared among instances of different close constructed types. This means that for a generic class C<T> which has a static field X, the values of C<int>.X and C<string>.X have completely different, independent values.

In the rare cases when you do need the 'specialized' static fields, feel free to suppress the warning.

If you need to have a static field shared between instances with different generic arguments, define a non-generic base class to store your static members, then set your generic type to inherit from this type.



回答3:

This is not necessarily an error - it is warning you about a potential misunderstanding of C# generics.

The easiest way to remember what generics do is the following: Generics are "blueprints" for creating classes, much like classes are "blueprints" for creating objects. (Well, this is a simplification though. You may use method generics as well.)

From this point of view MyClassRecipe<T> is not a class -- it is a recipe, a blueprint, of what your class would look like. Once you substitute T with something concrete, say int, string, etc., you get a class. It is perfectly legal to have a static member (field, property, method) declared in your newly created class (as in any other class) and no sign of any error here. It would be somewhat suspicious, at first sight, if you declare static MyStaticProperty<T> Property { get; set; } within your class blueprint, but this is legal too. Your property would be parameterized, or templated, as well.

No wonder in VB statics are called shared. In this case however, you should be aware that such "shared" members are only shared among instances of the same exact class, and not among the distinct classes produced by substituting <T> with something else.



回答4:

There are several good answers here already, that explain the warning and the reason for it. Several of these state something like having a static field in a generic type generally a mistake.

I thought I'd add an example of how this feature can be useful, i.e. a case where suppressing the R#-warning makes sense.

Imagine you have a set of entity-classes that you want to serialize, say to Xml. You can create a serializer for this using new XmlSerializerFactory().CreateSerializer(typeof(SomeClass)), but then you will have to create a separate serializer for each type. Using generics, you can replace that with the following, which you can place in a generic class that entities can derive from:

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

Since your probably don't want to generate a new serializer each time you need to serialize an instance of a particular type, you might add this:

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

If this class was NOT generic, then each instance of the class would use the same _typeSpecificSerializer.

Since it IS generic however, a set of instances with the same type for T will share a single instance of _typeSpecificSerializer (which will have been created for that specific type), while instances with a different type for T will use different instances of _typeSpecificSerializer.

An example

Provided the two classes that extend 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; }
}

... let's use them:

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

In this case, under the hood, firstInst and secondInst will be instances of the same class (namely SerializableEntity<MyFirstEntity>), and as such, they will share an instance of _typeSpecificSerializer.

thirdInst and fourthInst are instances of a different class (SerializableEntity<OtherEntity>), and so will share an instance of _typeSpecificSerializer that is different from the other two.

This means you get different serializer-instances for each of your entity types, while still keeping them static within the context of each actual type (i.e., shared among instances that are of a specific type).