C # статическое наследование членов - почему оно вообще существует?

В C # статические члены суперкласса «наследуются» в области видимости подклассов. Например:

class A { public static int M() { return 1; } }
class B : A {}
class C : A { public new static int M() { return 2; } }
[...]
A.M(); //returns 1
B.M(); //returns 1 - this is equivalent to A.M()
C.M(); //returns 2 - this is not equivalent to A.M()

Теперь вы не можете наследовать статические классы, и единственное место, где я могу представить, что статическое наследование может иметь значение, полностью игнорирует его: хотя вы можете сделать общее ограничение, которое требует, чтобы параметр типа T был подклассом A, вы все равно не можете вызвать T.M() (что, вероятно, упрощает работу виртуальной машины), не говоря уже о том, чтобы написать другую реализацию M в подклассе и использовать ее.

Итак, «наследование» статических членов просто выглядит как загрязнение пространства имен; даже если вы явно укажете имя (например, B.M), версия A все равно будет разрешена.

Изменить сравните с пространствами имен:

namespace N1{  class X();   }
namespace N1.N2 {  class X();   }
namespace N1.N2.N3 { [...] }

Внутри N1.N2.N3 Имеет смысл, что если я использую X без уточнения, это относится к N1.N2.X. Но если я прямо ссылаюсь на N1.N2.N3.X - а такого класса не существует - я не ожидаю, что он найдет версию N2; и действительно, компилятор сообщает об ошибке, если вы попробуете это сделать. Напротив, если я прямо ссылаюсь на B.M(), почему компилятор не сообщает об ошибке? В конце концов, в «Б» нет метода «М» ...

Какая цель у этого наследования? Можно ли как-то конструктивно использовать эту фичу?


person Eamon Nerbonne    schedule 17.02.2010    source источник
comment
+1 также следует запретить использование защищенных статических полей в статическом конструкторе наследника   -  person Sergey Mirvoda    schedule 17.02.2010
comment
Можете ли вы привести пример проблем с доступом к унаследованным защищенным статическим полям в статических конструкторах?   -  person Eamon Nerbonne    schedule 17.02.2010
comment
Я хочу прямо сейчас, чтобы во мне было истинное статическое наследование, и чтобы я мог указать public static abstract string Method();, например, требуя подклассов для реализации этого статического члена. Это сделало бы меня таким-таким счастливым!   -  person ErikE    schedule 16.03.2016
comment
@ErikE: (уродливый) обходной путь в некоторых случаях состоит в том, чтобы требовать параметр типа TStatic с ограничением where TStatic : struct, IMyMethod, а затем в коде потребления писать default(TStatic).Method(). Таким образом, вы можете хотя бы статически требовать наличия алгоритма, даже если синтаксис далеко не очевиден.   -  person Eamon Nerbonne    schedule 16.03.2016
comment
@EamonNerbonne, мне нужно подумать об этом! Я писал модульный тест, который находит все конкретные подклассы базового класса и гарантирует, что у них есть необходимые методы / свойства, которые принимают правильные параметры / типы и возвращают правильный тип. Я уверен, что практикуюсь в размышлениях. Написав класс typeprovider, который предоставляет параметры для реального модульного теста, я получаю по одному модульному тесту для каждого подтипа.   -  person ErikE    schedule 16.03.2016
comment
Этот пост дает хорошую альтернативу скрытию статического метода: Какая правильная альтернатива скрытию наследование статических методов?   -  person Hans Vonn    schedule 18.11.2019
comment
Согласен, в этом нет смысла. Статические члены не могут быть унаследованы, так почему пространство имен засорено именами, доступ к которым возможен только изнутри? Это особенно обременительно для общедоступных статических членов. Вынуждая нас использовать new иногда, чтобы скрыть / затенять унаследованный член, который на самом деле не может быть унаследован, чертовски раздражает.   -  person RBarryYoung    schedule 22.12.2020


Ответы (7)


Итак, «наследование» статических членов просто выглядит как загрязнение пространства имен.

Это верно, за исключением того, что загрязнение одного парня - это добавление пряностей другим парнем.

Я думаю, что Мартин Фаулер в своей работе над DSL предложил использовать наследование таким образом, чтобы обеспечить удобный доступ к статическим методам, позволяя использовать эти методы без уточнения имени класса. Таким образом, вызывающий код должен находиться в классе, наследующем класс, в котором определены методы. (Думаю, это дрянная идея.)

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

Особенно ужасно скрывать частные статические изменяемые данные внутри реализации класса "instancey". Но есть еще статические методы, которые еще хуже микшеры. Вот типичное использование статических методов, смешанных в классе:

public class Thing
{
    // typical per-instance stuff
    int _member1;
    protected virtual void Foo() { ... }
    public void Bar() { ... }

    // factory method
    public static Thing Make()
    {
        return new Thing();
    }
}

Это шаблон статического фабричного метода. В большинстве случаев это бессмысленно, но еще хуже то, что теперь у нас есть это:

public class AnotherThing : Thing { }

Теперь у него есть статический метод Make, который возвращает Thing, а не AnotherThing.

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

Но это всего лишь одна из тех вещей, о которых сейчас уже слишком поздно. Все настоящие рабочие языки (и библиотеки, и продукты) имеют несколько из них. В C # их очень мало.

person Daniel Earwicker    schedule 18.02.2010
comment
Здесь есть две разные проблемы: разрешаете ли вы простой M(); откуда-то из B, и, во-вторых, что происходит, когда вы явно говорите B.M(); Я считаю, что аргумент для разрешения этих методов без уточнения имени класса (вроде как методы расширения) быть разумно, но я не вижу смысла заново интерпретировать явные квалификации пространства имен. - person Eamon Nerbonne; 18.02.2010
comment
Я тоже согласен с этим, не только потому, что это бессмысленно, но на самом деле может вводить в заблуждение, особенно если статические методы похожи на фабричные - см. Update. - person Daniel Earwicker; 19.02.2010
comment
как насчет public static T Make<T>() where T : Thing, new() { return new T(); }? - person Nick Strupat; 05.03.2014
comment
@NickStrupat - Почему вы использовали бы это, а не оператор new напрямую? - person Daniel Earwicker; 06.03.2014
comment
Я не уверен. Думаю, если вы по какой-то причине используете фабричный паттерн. Я просто думал в контексте вашего ответа. - person Nick Strupat; 06.03.2014
comment
Загрязнение пространства имён одного парня - это добавление пикантности другому парню. Думаю, это моя новая любимая цитата за все время. - person Asad Saeeduddin; 31.08.2014
comment
@NickStrupat проблема с универсальным методом, который вы описываете, заключается в том, что вам нужно знать тип, который вы хотите вернуть, при его вызове, но если вы уже знаете, какой тип вы хотите вернуть, почему бы не использовать конструктор типа напрямую? - person Kaveh Hadjari; 15.08.2016

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

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

Конечно, они не передаются по наследству, это просто ярлык

person Luis Filipe    schedule 18.02.2010
comment
В этом есть смысл - это сокращенный помощник для intellisense. Конечно, intellisense работает лучше всего, если у вас есть только полезные вещи, так что это не чистая победа. - person Eamon Nerbonne; 18.02.2010
comment
Мне бы хотелось увидеть, как intellisense предоставляет фильтры, такие как, позвольте мне видеть только события, только расширения и т. Д. Может быть, VS2010 RC1 имеет это ... не проверяйте это еще - person Luis Filipe; 18.02.2010
comment
Недавно я узнал, что R # предупреждает вас при ссылке на статический член из унаследованного класса. - person Luis Filipe; 07.08.2013

Вот как это работает, в большинстве случаев, наверное, был бы глупым ответом. Но в данном случае это работает так; поскольку вы производите от A, вы говорите, что вы A + добавляемые вами дополнительные функции.

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

Однако наследование статического класса не имеет смысла, в то время как доступ к статическим членам / полям / методам имеет.

Примером этого является следующее:

internal class BaseUser
{
    public static string DefaultUserPool { get; set; }
}
internal class User : BaseUser
{
    public int Id { get; set; }
    public string Name { get; set; }
    public User Parent { get; set; }
}

Где тест выглядит так:

User.DefaultUserPool = "Test";
BaseUser.DefaultUserPool = "Second Test";

Console.WriteLine(User.DefaultUserPool);
Console.WriteLine(BaseUser.DefaultUserPool);

Обе строки WriteLines выводят «Второй тест», потому что и BaseUser, и User должны использовать DefaultUserPool, по замыслу. А переопределение статических реализованных методов не имело бы большого смысла, поскольку это всего лишь метод доступа в дочернем классе.

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

person Filip Ekberg    schedule 17.02.2010
comment
Вопрос в том, почему User.DefaultUserPool вообще компилируется. Насколько я могу судить, это просто сбивает с толку; например, как указывает ваш пример, на первый взгляд не очевидно, ссылаются ли эти две отдельные статические ссылки на одну и ту же базовую переменную. Если бы у пользователя был собственный пул DefaultUserPool (либо в другой сборке, либо с ключевым словом new), этот код также компилировался бы без предупреждения, но приводил бы к другому выводу - короче говоря, вы можете эффективно переопределить статический член (даже if для статического случая переопределение и скрытие эквивалентны). - person Eamon Nerbonne; 17.02.2010
comment
Другими словами, как возможность сказать User.DefaultUserPool добавить что-нибудь к необходимости писать BaseUser.DefaultUserPool? В конце концов, в классе User нет элемента или свойства DefaultUserPool, он наследует это от BaseUser. - person Eamon Nerbonne; 17.02.2010
comment
В более абстрактной манере это имеет смысл, в более конкретной / реализационной манере это может не иметь такого большого смысла .. Но это зависит от того, как вы на это смотрите, User Extends BaseUser и, следовательно, у вас есть доступ ко всем общедоступным вещам из BaseUser. На самом деле нет никакой разницы в наличии нестатического и статического в этом вопросе, единственная разница заключается в том, что вы можете получить к нему доступ, используя как BaseUser, так и User, оба из них будут ссылаться на один и тот же объект, так как это все, что статика, фрагмент данных, хранящийся в течение всего жизненного цикла приложения. - person Filip Ekberg; 17.02.2010
comment
Если это интуиция, почему я не могу получить к ней доступ из общего кода? Я могу гарантировать во время компиляции, что все подтипы A будут иметь некоторый статический метод M - так почему я не могу его вызвать? И эта интуиция отличается от интуиции, используемой для статических членов конкретных версий универсальных типов - то есть, если MyType<T> имеет статический член, этот член отличается для MyType<int> и MyType<bool> (конечно, универсальные типы не совсем подклассы - мы говорим дизайн, здесь). И, что еще хуже, что если MyType<T> : A - тогда MyType<int>.M() действительно относится к тому же методу, что и MyType<bool>.M(). - person Eamon Nerbonne; 18.02.2010
comment
В своем предыдущем комментарии я пытаюсь понять не то, что ваша идея не имеет смысла - она ​​имеет смысл - просто она не полностью реализована (например, не используются дженерики), и я не вижу цель сделать это таким образом - почему. - person Eamon Nerbonne; 18.02.2010
comment
MyType ‹int› .M () и MyType ‹bool› .M () будут ссылаться на один и тот же статический метод, потому что не имеет значения, какой тип Generic на самом деле является T, определяет только тип, используемый внутри объекта, а не Что такое объекты. Поэтому, когда MyType ‹T› является производным от A, а A имеет статический член / метод, вы всегда сможете получить доступ к M () через MyType, несмотря на то, чем T когда-либо будет. - person Filip Ekberg; 18.02.2010
comment
Это моя точка зрения - иногда MyType<X>.Q === MyType<Y>.Q, в зависимости от того, определено ли A в MyType<> или в базовом типе. Напротив, если бы статические члены не были унаследованы, это было бы невозможно. - person Eamon Nerbonne; 18.02.2010
comment
Не забывайте, что MyType ‹X› и MyType ‹Y› по-прежнему являются MyType, и если MyType наследуется от A и имеет статическую функциональность, они будут унаследованы / на них можно ссылаться. - person Filip Ekberg; 18.02.2010

Собственно, насколько я понимаю, это всего лишь ярлык, предоставленный компилятором. Синтаксический сахар. B.M() просто компилируется в A.M(), поскольку B не имеет static M(), а A имеет. Это для облегчения написания, не более того. Нет никакого «статического наследования».

Добавлено: И требование для new при "переопределении" просто так, чтобы вы случайно не прострелили себе ногу.

person Vilx-    schedule 18.02.2010
comment
Но почему? Вы могли разработать язык, в котором, если символ не идентифицирован, он выбирает символ в области видимости, ближайший по имени по расстоянию Левенштейна, разрешая связи лексикографически. Возможно наличие псевдонимов, относящихся к одному и тому же объекту, но в этом случае почему? - person Eamon Nerbonne; 18.02.2010

Я думаю, это для доступа к защищенным статическим членам базового класса.

class Base
{
    protected static void Helper(string s)
    {
       Console.WriteLine(s);
    }
}

class Subclass : Base
{
   public void Run()
    {
       Helper("From the subclass");
    }
}
person TrueWill    schedule 17.02.2010
comment
Это не вызывает сомнений - внутри подкласса ясно, что Helper и Base.Helper (и даже base.Helper) разрешаются в метод Helper класса Base. Однако менее понятно, почему Subclass.Helper работает. - person Eamon Nerbonne; 18.02.2010

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

игнорируйте приведенное выше, по какой-то причине я думал о запечатанном, а не о статическом

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

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

person ChrisBD    schedule 17.02.2010
comment
Это делает ключевое слово sealed. На самом деле это не так; наследующий класс может фактически заменить реализацию базовых классов, что мы обычно называем переопределением. - person Eamon Nerbonne; 17.02.2010
comment
@Eamon - вы совершенно правы. У меня там был мозговой штурм. - person ChrisBD; 17.02.2010

Итак ... Какая альтернатива?

В вопросе упоминается ...

почему компилятор не сообщает об ошибке? В конце концов, в B нет метода M ...

Но есть производный метод M в классе B.

Если бы компилятор не представил программисту единую виртуальную таблицу для базовых случаев, тогда программисту пришлось бы искать статические методы среди базовых типов. Это нарушило бы полиморфизм.

Википедия ...

Полиморфизм подтипа, почти повсеместно называемый просто полиморфизмом в контексте объектно-ориентированного программирования, - это способность одного типа, A, появляться и использоваться как другой тип, B ....

В строго типизированных языках полиморфизм обычно означает, что тип A каким-то образом является производным от типа B или что тип C реализует интерфейс, представляющий тип B.

person kervin    schedule 18.02.2010
comment
Этот аргумент справедлив для нестатических членов; только wrt экземпляры являются типами, в первую очередь, полиморфными (вики-обзор вводит в заблуждение). Если бы статические члены были полиморфными, я бы ожидал (например) работы void CallM<T>() where T:A {T.M();}. В любом случае вы не можете на самом деле вызвать / использовать статический член через экземпляр, поэтому отсутствие статического наследования не нарушает полиморфизм (т.е. вы не можете делать new A().M();), и нет общего правила, что вы должны уметь использовать под тип везде, где можно использовать базовый класс - например, параметры контравариантны. - person Eamon Nerbonne; 18.02.2010
comment
Альтернативой было бы разрешить A.M(); и M(); (точно так же, как и пространства имен) и выдать ошибку компиляции в строке B.M(); - person Eamon Nerbonne; 18.02.2010
comment
@EamonNerbonne: Было бы полезно, если бы методы могли указывать, должны ли и как они должны быть видны унаследованным классам. Если Foo является широко используемым классом и оказывается необходимым иметь тип FooBase, от которого происходит тип Bar (без Bar потомка Foo), чтобы можно было перемещать статические члены в FooBase без нарушения кода, который их вызывает from Foo полезно, но бывают случаи, когда методы действительно следует вызывать только с использованием базового класса. - person supercat; 14.07.2015