Специализация по стандарту С#

Интересно, можно ли каким-либо образом специализировать методы интерфейса в С#? Я нашел похожие вопросы, но ничего подобного. Теперь я подозреваю, что ответ "Нет, вы не можете", но я хотел бы, чтобы это подтвердилось.

У меня есть что-то вроде следующего.

public interface IStorage
{
    void Store<T>(T data);
}

public class Storage : IStorage
{
    public void Store<T>(T data)
    {
        Console.WriteLine("Generic");
    }

    public void Store(int data)
    {
        Console.WriteLine("Specific");
    }
}

class Program
{
    static void Main(string[] args)
    {
        IStorage i = new Storage();
        i.Store("somestring"); // Prints Generic
        i.Store(1); // Prints Generic
        Storage s = (Storage)i;
        s.Store("somestring"); // Prints Generic
        s.Store(1); // Prints Specific
    }
}

Можно ли использовать специализированную версию Store при вызове через интерфейс? А если нет, то кто-нибудь знает точную причину, почему С# рассматривает общие аргументы таким образом?

Изменить: Проблема может быть решена, если это не так, что С# не может разрешить аргументы шаблона более чем на один шаг.

void Foo<T>(T t)
{
    SubFoo(t);
}

void SubFoo<T>(T t)
{
    Console.WriteLine("Generic");
}

void SubFoo(int t)
{
    Console.WriteLine("Specific");
}

Вызов Foo (1) здесь также напечатает "Generic", не должен ли компилятор разрешить это? Или JIT предотвращает это?

Ответы

Ответ 1

Разрешение перегрузки выполняется во время компиляции, а не во время выполнения на основе фактического типа переданного значения.

IStorage i = new Storage();
i.Store("somestring"); // Prints Generic
i.Store(1); // Prints Generic

Это всегда будет вызывать метод "generic", потому что в IStorage есть только одна перегрузка Store, и компилятор не знает, что i фактически содержит объект Storage. Как компилятор знает о другой перегрузке в Storage?

Storage s = (Storage)i;
s.Store("somestring"); // Prints Generic
s.Store(1); // Prints Specific

Здесь компилятор знает, что s содержит объект Storage (или один из Storage), потому что s объявлен таким образом. Таким образом, он видит две перегрузки. Он выбирает конкретную перегрузку для значений int, поскольку правила разрешения перегрузки говорят, что предпочитают конкретные перегрузки по общим перегрузкам.


Технически можно определить typeof(T) в общем методе во время выполнения и перенаправить вызов метода на конкретный метод. Но если вы думаете об этом, это не имеет большого смысла. Общий метод означает, что одна и та же реализация работает для аргументов разных несвязанных типов. Если вам нужны разные реализации для разных типов, вы не должны использовать для этого generics.


void Foo<T>(T t)
{
    SubFoo(t);
}

void SubFoo<T>(T t);
void SubFoo(int t);

Работа с генериками немного отличается от шаблонов С++. Компилятор С# компилирует Foo только один раз - в общий метод. Помните: generic означает ту же реализацию для разных типов. Компилятор С# не знает во время компиляции, если T будет int или string или любым другим типом. Таким образом, единственная возможная реализация Foo, которая работает для любого T, - это вызов SubFoo <T> . Если одна из перегрузок SubFoo будет вызываться в зависимости от T, реализация Foo не будет одинаковой для всех T.

Ответ 2

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

Я возьму пример для коллекций, потому что evrybody называет более или менее коллекций .NET.

Я возьму простой пример метода расширения .Last(this IEnumerable<<T>> coll). В .NET Framework этот метод использует специализацию по типу кода.

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

Во-вторых, поскольку этот метод является статическим, наличие многих реализаций для каждого типа коллекции или интерфейсов не решит проблему выбора правильного метода. Фактически, выбор правильного метода выполняется во время компиляции на основе видимого типа объекта coll. Если вы предполагаете, вы хотите применить последовательные методы расширений на List<<T>>, первой может не понадобиться много для специализированных реализаций типа коллекции и использовать один на основе IEnumerable<<T>>. Поэтому, даже если у нас есть .Last(this List<<T>> coll), первый неспециализированный метод расширения вернет IEnumerable<<T>>, а специализированный .Last(this List<<T>> coll) не будет использоваться для List<<T>>.

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

Ответ 3

Если вы хотите воспользоваться разрешением перегрузки во время компиляции, вы также можете расширить интерфейс с помощью метода, который принимает int:

public interface IStorage
{
    void Store<T>(T data);
}

public interface IIntStorage: IStorage
{
    void Store(int data);
}

public class Storage : IIntStorage
{
    public void Store<T>(T data)
    {
        Console.WriteLine("Generic");
    }

    public void Store(int data)
    {
        Console.WriteLine("Specific");
    }
}

Теперь, если вы вызываете Store(1) через интерфейс IIntStorage, он будет использовать специализированный метод (аналогично тому, как вы вызвали метод Storage напрямую), но если вы вызовете его через IStorage, он все равно будет использовать общий версия.

Ответ 4

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

public interface IStorage<T>
{
    void Store(object data);
    void Store<T>(T data);
}

public class Storage : IStorage<int>
{
    public void Store(object data)
    {
        Console.WriteLine("Generic");
    }

    public void Store(int data)
    {
        Console.WriteLine("Specific");
    }
}

Вы набрали я как IStorage, и этот интерфейс не определяет перегруженный метод Store.

Ответ 5

Поскольку С# generics - это шаблоны времени выполнения, в некоторых случаях вы должны использовать специализацию во время выполнения. Например, в общих статических методах наследование и интерфейсы непригодны для использования. Если вы хотите специализировать общие статические методы - в частности, методы расширения - вам нужно определить тип кода с помощью таких конструкций, как:

if (typeof (T) == typeof (bool))

Для специализации ссылочных типов (например, строки, например) и данных аргумента T вы бы предпочли:

string s = данные как строка; if (s!= null)

В этом примере проблема возникает из преобразования между T и bool в специализированном коде: вы знаете, что T является bool, но язык не позволяет преобразовать эти типы. Решение исходит от типа объекта: объект может быть передан любому типу (преобразование проверяется во время выполнения, а не во время компиляции). поэтому, если у вас есть

T данных;

вы можете написать:

bool b = (bool) (объект); Данные = (Т) (объект) б;

Это не идеально: если равенство типа довольно быстро, в некоторых случаях вам нужно проверить, является ли T производным типом указанного типа (немного дольше). И когда T является типом значения типа bool, отбрасывается на объект, а затем возвращается к типу типа среднего типа бокс/распаковка и проверка типа выполнения для ссылочных типов. Оптимизатор времени выполнения может удалить эти необязательные шаги, но я не могу сказать, делают ли они это.

В зависимости от использования вашего статического метода помните, что вы можете применять ограничения T:... для параметризованных типов. И это значение по умолчанию (T) возвращает false для boolean, ноль для числовых базовых типов и null для ссылочных типов.

Специализация Runtime подразумевает дополнительные этапы тестирования и проверки типа бокса/распаковки/времени выполнения, поэтому это не панацея, но позволяет во многих случаях слишком специализировать общие методы в приемлемое время: для длительной работы (в частности, для оптимизации) или когда скрытие или группировка типов сложность управления важнее, чем показатели.