ReSharper предупреждает: «Статическое поле в универсальном типе»

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

Это неправильно? Я хотел бы предположить, что это на самом деле имеет static readonlyполе для каждого из возможных EnumRouteConstraint<T>случаев, которые я случайно произвел.

bevacqua
источник
Иногда это особенность, иногда раздражение. Я хотел бы, чтобы у C # было какое-то ключевое слово, чтобы отличить их
nawfal

Ответы:

468

Хорошо иметь статическое поле в универсальном типе, если вы знаете, что вы действительно получите одно поле на комбинацию аргументов типа. Я думаю, что R # просто предупреждает вас, если вы не знали об этом.

Вот пример этого:

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

Как видите, Generic<string>.Fooполе отличается от Generic<object>.Foo- они содержат отдельные значения.

Джон Скит
источник
Это также верно, когда универсальные классы наследуются от неуниверсального класса, который содержит статические типы. Например, если я создаю class BaseFooсодержащий статический член, то получим ли из него class Foo<T>: BaseFooвсе Foo<T>классы одинаковое значение статического члена?
bikeman868
2
Отвечая на мой собственный комментарий, но да, все Foo <T> будут иметь одинаковое статическое значение, если они содержатся в неуниверсальном базовом классе. См. Dotnetfiddle.net/Wz75ya
bikeman868
147

Из вики JetBrains :

В подавляющем большинстве случаев наличие статического поля в универсальном типе является признаком ошибки. Причина этого заключается в том, что статическое поле в универсальном типе не будет совместно использоваться экземплярами различных закрытых типов. Это означает, что для универсального класса, C<T>который имеет статическое поле X, значения C<int>.Xи C<string>.X имеют совершенно разные, независимые значения.

В тех редких случаях , когда вы действительно нуждаетесь в «специализированные» статических полях, не стесняйтесь , чтобы подавить предупреждение.

Если вам нужно иметь статическое поле, совместно используемое между экземплярами с различными универсальными аргументами, определите неуниверсальный базовый класс для хранения ваших статических членов, а затем установите свой универсальный тип для наследования от этого типа.

AakashM
источник
13
При использовании универсального типа, технически вы получаете отдельный и отдельный класс для каждого универсального типа, который вы размещаете. Когда вы объявляете два отдельных неуниверсальных класса, вы не ожидаете, что между ними будут использоваться статические переменные, так почему дженерики должны отличаться? Единственный способ, которым это можно считать редким, - это если большинство разработчиков не понимают, что они делают при создании универсальных классов.
Syndog
2
@ Syndog описанное поведение статики в общем классе выглядит хорошо и понятно для меня. Но я полагаю, что причина этих предупреждений в том, что не у каждой команды есть только опытные и целенаправленные разработчики. Правильный код становится подверженным ошибкам из-за квалификации разработчика.
Стас Иванов
Но что, если я не хочу создавать неуниверсальный базовый класс просто для хранения этих статических полей. Могу ли я просто подавить предупреждения, в этом случае?
Том Линт
@TomLint, если вы знаете, что делаете, то подавление предупреждений - это действительно то, что нужно сделать.
AakashM
65

Это не обязательно ошибка - это предупреждение о возможном недопонимании обобщений C #.

Самый простой способ запомнить, что делают дженерики, это следующее: дженерики - это «чертежи» для создания классов, так же как классы являются «чертежами» для создания объектов. (Ну, это упрощение. Вы также можете использовать обобщенные методы).

С этой точки зрения MyClassRecipe<T>это не класс, это рецепт, план того, как будет выглядеть ваш класс. Как только вы замените T чем-то конкретным, скажем, int, string и т. Д., Вы получите класс. Вполне допустимо, чтобы статический член (поле, свойство, метод) был объявлен во вновь созданном классе (как и в любом другом классе), и здесь не было никаких признаков какой-либо ошибки. На первый взгляд, было бы несколько подозрительно, если бы вы объявили в static MyStaticProperty<T> Property { get; set; }рамках своего класса, но это тоже законно. Ваша собственность будет параметризована или шаблонизирована.

Недаром в VB статики называются shared. В этом случае, однако, вы должны знать, что такие «общие» члены разделяются только между экземплярами одного и того же класса, а не между отдельными классами, полученными путем замены <T>чем-то другим.

Александр Христов
источник
1
Я думаю, что имя C ++ делает его понятнее всего. В C ++ они называются шаблонами, что и есть, шаблонами для конкретных классов.
Майкл Браун
8

Здесь уже есть несколько хороших ответов, которые объясняют предупреждение и причину его возникновения. Некоторые из них указывают на то, что статическое поле общего типа обычно является ошибкой .

Я думал, что добавлю пример того, как эта функция может быть полезна, то есть случай, когда подавление предупреждения R # имеет смысл.

Представьте, что у вас есть набор классов сущностей, которые вы хотите сериализовать, скажем, в Xml. Для этого вы можете создать сериализатор new XmlSerializerFactory().CreateSerializer(typeof(SomeClass)), но тогда вам придется создать отдельный сериализатор для каждого типа. Используя дженерики, вы можете заменить их следующим, который вы можете поместить в дженерик-класс, из которого могут быть получены объекты:

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

Поскольку вы, вероятно, не хотите генерировать новый сериализатор каждый раз, когда вам нужно сериализовать экземпляр определенного типа, вы можете добавить это:

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

Если этот класс НЕ является универсальным, то каждый экземпляр класса будет использовать то же самое _typeSpecificSerializer.

Однако, поскольку он универсален, набор экземпляров с одинаковым типом for Tбудет использовать один экземпляр _typeSpecificSerializer(который будет создан для этого конкретного типа), а экземпляры с другим типом for Tбудут использовать разные экземпляры _typeSpecificSerializer.

Пример

Предусмотрено два класса, которые расширяются 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; }
}

... давайте использовать их:

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

В этом случае под капотом firstInstи secondInstбудут экземпляры одного класса (а именно SerializableEntity<MyFirstEntity>), и как таковые они будут совместно использовать экземпляр _typeSpecificSerializer.

thirdInstи fourthInstявляются экземплярами другого класса ( SerializableEntity<OtherEntity>), и поэтому будут использовать экземпляр, _typeSpecificSerializerкоторый отличается от двух других.

Это означает, что вы получаете разные экземпляры сериализатора для каждого из ваших типов сущностей , сохраняя при этом их статичность в контексте каждого фактического типа (т. Е. Разделяемые между экземплярами определенного типа).

Kjartan
источник
Из-за правил статической инициализации (статический инициализатор не вызывается до тех пор, пока класс не будет впервые указан), вы можете отказаться от проверки в Getter и просто инициализировать ее в объявлении статического экземпляра.
Майкл Браун