Почему я не могу использовать абстрактные статические методы в C#?

В последнее время я изрядно работал с поставщиками и столкнулся с интересной ситуацией, когда я хотел иметь абстрактный класс с абстрактным статическим методом. Я прочитал несколько сообщений по этой теме, и это имело смысл, но есть ли хорошее ясное объяснение?

Ответов (8)

Решение

Статические методы не создаются как таковые, они просто доступны без ссылки на объект.

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

Приведу пример.

Со следующим кодом:

public class A
{
    public static void Test()
    {
    }
}

public class B : A
{
}

Если вы вызовете B.Test, вот так:

class Program
{
    static void Main(string[] args)
    {
        B.Test();
    }
}

Тогда фактический код внутри метода Main выглядит следующим образом:

.entrypoint
.maxstack 8
L0000: nop 
L0001: call void ConsoleApplication1.A::Test()
L0006: nop 
L0007: ret 

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

Если бы у вас были типы классов , например, в Delphi, где вы можете создать переменную, относящуюся к типу, а не объекту, вы бы больше использовали виртуальные и, следовательно, абстрактные статические методы (а также конструкторы), но они недоступны и таким образом, статические вызовы в .NET не являются виртуальными.

Я понимаю, что разработчики IL могут позволить скомпилировать код для вызова B.Test и разрешить вызов во время выполнения, но он все равно не будет виртуальным, так как вам все равно придется писать там какое-то имя класса.

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

Таким образом, виртуальные / абстрактные статические методы недоступны в .NET.

Этому вопросу 12 лет, но он все еще требует лучшего ответа. Как немногие отметили в комментариях и вопреки тому, что утверждают все ответы, безусловно, имеет смысл иметь статические абстрактные методы в C#. Как сказал философ Дэниел Деннетт, недостаток воображения - это не понимание необходимости. Распространенная ошибка - не осознавать, что C# - это не только язык ООП. Чистая точка зрения ООП на данную концепцию приводит к ограниченному и, в данном случае, ошибочному анализу. Полиморфизм - это не только разделение полиморфизма на подтипы: он также включает параметрический полиморфизм (также известный как универсальное программирование), и C# уже давно поддерживает это. В рамках этой дополнительной парадигмы абстрактные классы (и большинство типов) используются не только для ввода экземпляров. Их также можно использовать какграницы для общих параметров ; то, что было понятно пользователям определенных языков (например, Haskell, а в последнее время Scala, Rust или Swift) в течение многих лет.

В этом контексте вы можете сделать что-то вроде этого:

void Catch<TAnimal>() where TAnimal : Animal
{
    string scientificName = TAnimal.ScientificName; // abstract static property
    Console.WriteLine($"Let's catch some {scientificName}");
    …
}

И здесь возможность выражать статические члены, которые могут быть специализированы подклассами, имеет смысл !

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

Основная идея состоит в том, чтобы связать абстрактный класс-компаньон (здесь SpeciesFor<TAnimal> ) с тем, который должен содержать статические абстрактные члены (здесь Animal ):

public abstract class SpeciesFor<TAnimal> where TAnimal : Animal
{
    public static SpeciesFor<TAnimal> Instance { get { … } }

    // abstract "static" members

    public abstract string ScientificName { get; }
    
    …
}

public abstract class Animal { … }

Теперь мы хотели бы поработать:

void Catch<TAnimal>() where TAnimal : Animal
{
    string scientificName = SpeciesFor<TAnimal>.Instance.ScientificName;
    Console.WriteLine($"Let's catch some {scientificName}");
    …
}

Конечно, нам нужно решить две проблемы:

  1. Как мы разрешаем и заставляем разработчика подкласса Animalсвязывать конкретный экземпляр SpeciesFor<TAnimal>с этим подклассом?
  2. Как свойство SpeciesFor<TAnimal>.Instanceполучает эту информацию?

Вот как мы можем решить 1:

public abstract class Animal<TSelf> where TSelf : Animal<TSelf>
{
    private Animal(…) {}
    
    public abstract class OfSpecies<TSpecies> : Animal<TSelf>
        where TSpecies : SpeciesFor<TSelf>, new()
    {
        protected OfSpecies(…) : base(…) { }
    }
    
    …
}

Сделав конструктор Animal<TSelf> закрытым, мы гарантируем, что все его подклассы также являются подклассами внутреннего класса Animal<TSelf>.OfSpecies<TSpecies> . Таким образом, эти подклассы должны указывать TSpecies тип, который имеет new() границу.

Для 2 мы можем предоставить следующую реализацию:

public abstract class SpeciesFor<TAnimal> where TAnimal : Animal<TAnimal>
{
    private static SpeciesFor<TAnimal> _instance;

    public static SpeciesFor<TAnimal> Instance => _instance ??= MakeInstance();

    private static SpeciesFor<TAnimal> MakeInstance()
    {
        Type t = typeof(TAnimal);
        while (true)
        {
            if (t.IsConstructedGenericType
                    && t.GetGenericTypeDefinition() == typeof(Animal<>.OfSpecies<>))
                return (SpeciesFor<TAnimal>)Activator.CreateInstance(t.GenericTypeArguments[1]);
            t = t.BaseType;
            if (t == null)
                throw new InvalidProgramException();
        }
    }

    // abstract "static" members

    public abstract string ScientificName { get; }
    
    …
}

Как мы можем быть уверены, что код отражения внутри MakeInstance() никогда не сработает? Как мы уже говорили, почти все классы в иерархии Animal<TSelf> также являются подклассами Animal<TSelf>.OfSpecies<TSpecies> . Итак, мы знаем, что для этих классов TSpecies должны быть предусмотрены определенные. Этот тип также обязательно может быть сконструирован благодаря ограничению : new() . Но это по-прежнему оставляет такие абстрактные типы Animal<Something>, которые не имеют ассоциированных видов. Теперь мы можем убедить себя в том, что любопытно повторяющийся шаблон шаблона where TAnimal : Animal<TAnimal> делает невозможным запись, SpeciesFor<Animal<Something>>.Instance поскольку тип Animal<Something> никогда не является подтипом Animal<Animal<Something>> .

И вуаля:

public class CatSpecies : SpeciesFor<Cat>
{
    // overriden "static" members

    public override string ScientificName => "Felis catus";
    public override Cat CreateInVivoFromDnaTrappedInAmber() { … }
    public override Cat Clone(Cat a) { … }
    public override Cat Breed(Cat a1, Cat a2) { … }
}

public class Cat : Animal<Cat>.OfSpecies<CatSpecies>
{
    // overriden members

    public override string CuteName { get { … } }
}

public class DogSpecies : SpeciesFor<Dog>
{
    // overriden "static" members

    public override string ScientificName => "Canis lupus familiaris";
    public override Dog CreateInVivoFromDnaTrappedInAmber() { … }
    public override Dog Clone(Dog a) { … }
    public override Dog Breed(Dog a1, Dog a2) { … }
}

public class Dog : Animal<Dog>.OfSpecies<DogSpecies>
{
    // overriden members

    public override string CuteName { get { … } }
}

public class Program
{
    public static void Main()
    {
        ConductCrazyScientificExperimentsWith<Cat>();
        ConductCrazyScientificExperimentsWith<Dog>();
        ConductCrazyScientificExperimentsWith<Tyranosaurus>();
        ConductCrazyScientificExperimentsWith<Wyvern>();
    }
    
    public static void ConductCrazyScientificExperimentsWith<TAnimal>()
        where TAnimal : Animal<TAnimal>
    {
        // Look Ma! No animal instance polymorphism!
        
        TAnimal a2039 = SpeciesFor<TAnimal>.Instance.CreateInVivoFromDnaTrappedInAmber();
        TAnimal a2988 = SpeciesFor<TAnimal>.Instance.CreateInVivoFromDnaTrappedInAmber();
        TAnimal a0400 = SpeciesFor<TAnimal>.Instance.Clone(a2988);
        TAnimal a9477 = SpeciesFor<TAnimal>.Instance.Breed(a0400, a2039);
        TAnimal a9404 = SpeciesFor<TAnimal>.Instance.Breed(a2988, a9477);
        
        Console.WriteLine(
            "The confederation of mad scientists is happy to announce the birth " +
            $"of {a9404.CuteName}, our new {SpeciesFor<TAnimal>.Instance.ScientificName}.");
    }
}

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

Чтобы добавить к предыдущим объяснениям, вызовы статических методов привязаны к определенному методу во время компиляции , что скорее исключает полиморфное поведение.

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

Предположим, вы можете на мгновение унаследовать статические методы. Представьте себе такой сценарий:

public static class Base
{
    public static virtual int GetNumber() { return 5; }
}

public static class Child1 : Base
{
    public static override int GetNumber() { return 1; }
}

public static class Child2 : Base
{
    public static override int GetNumber() { return 2; }
}

Если вы вызовете Base.GetNumber (), какой метод будет вызван? Какое значение вернулось? Довольно легко увидеть, что без создания экземпляров объектов наследование довольно сложно. Абстрактные методы без наследования - это просто методы, у которых нет тела, поэтому их нельзя вызывать.

Другой респондент (Макдауэлл) сказал, что полиморфизм работает только для экземпляров объектов. Это должно быть оговорено; есть языки, которые рассматривают классы как экземпляры типа «Класс» или «Метакласс». Эти языки поддерживают полиморфизм как для методов экземпляра, так и для методов класса (статических).

C#, как Java и C++ до него, не является таким языком; static ключевое слово используется для обозначения явно , что метод статический переплетом , а не динамический / виртуальный.

На самом деле мы переопределяем статические методы (в delphi), это немного некрасиво, но отлично работает для наших нужд.

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

class function AvailableObjects: string; override;
begin
  Result := 'Object1, Object2';
end; 

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

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

Так что поддерживать это намного проще, чем иметь одно отдельное серверное приложение для каждого клиента.

Надеюсь, пример был ясен.

Абстрактные методы неявно виртуальны. Абстрактные методы требуют экземпляра, но статические методы не имеют экземпляра. Итак, у вас может быть статический метод в абстрактном классе, он просто не может быть статическим абстрактным (или абстрактным статическим).

Вот ситуация, когда определенно необходимо наследование для статических полей и методов:

abstract class Animal
{
  protected static string[] legs;

  static Animal() {
    legs=new string[0];
  }

  public static void printLegs()
  {
    foreach (string leg in legs) {
      print(leg);
    }
  }
}


class Human: Animal
{
  static Human() {
    legs=new string[] {"left leg", "right leg"};
  }
}


class Dog: Animal
{
  static Dog() {
    legs=new string[] {"left foreleg", "right foreleg", "left hindleg", "right hindleg"};
  }
}


public static void main() {
  Dog.printLegs();
  Human.printLegs();
}


//what is the output?
//does each subclass get its own copy of the array "legs"?