Ограничение параметров универсального типа определенным конструктором - PullRequest
19 голосов
/ 16 марта 2012

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


Аргумент 1: конструкторы методы

@ Эрик: Позвольте мне побыть здесь с вами на секунду:

Конструкторы - это методы

Тогда я полагаю, что никто не будет возражать, если я пойду так:

public interface IReallyWonderful
{
    new(int a);

    string WonderMethod(int a);
}

Но как только я получу это, я пойду:

public class MyClass<T>
        where T : IReallyWonderful
{
    public string MyMethod(int a, int b)
    {
        T myT = new T(a);
        return myT.WonderMethod(b);
    }
}

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

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

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

Должны ли конструкторы рассматриваться как часть реализации класса или как часть семантического контракта (так же, как интерфейс считается семантическим контрактом).

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

Пример конструктора как части реализации (нет смысла указывать любой из следующих конструкторов как часть семантического контракта, определенного ITransformer):

public interface ITransformer
{
    //Operates with a and returns the result;
    int Transform(int a);
}

public class PlusOneTransformer : ITransformer
{
    public int Transform(int a)
    {
        return a + 1;
    }
}

public class MultiplyTransformer : ITransformer
{
    private int multiplier;

    public MultiplyTransformer(int multiplier)
    {
        this.multiplier = multiplier;
    }

    public int Transform(int a)
    {
        return a * multiplier;
    }
}

public class CompoundTransformer : ITransformer
{
    private ITransformer firstTransformer;
    private ITransformer secondTransformer;

    public CompoundTransformer(ITransformer first, ITransformer second)
    {
        this.firstTransformer = first;
        this.secondTransformer = second;
    }

    public int Transform(int a)
    {
        return secondTransformer.Transform(firstTransformer.Transform(a));
    }
}

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

public interface ICollection<T> : IEnumerable<T>
{
    new(IEnumerable<T> tees);

    void Add(T tee);

    ...
}

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

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


Аргумент 2: предполагаемые проблемы при разрешении конструкторов

@ supercat пытается привести несколько примеров того, как (цитата из комментария)

Было бы также трудно точно определить, как должны работать ограничения конструктора, не приводя к неожиданному поведению.

но я действительно должен не согласиться. В C # (ну, в .NET) такие сюрпризы, как «Как заставить пингвина летать?» просто не бывает Существуют довольно простые правила относительно того, как компилятор разрешает вызовы методов, и если компилятор не может разрешить это, ну, он не пройдет, не скомпилирует это.

Его последний пример был:

Если они противоречивы, то возникает проблема с разрешением того, какой конструктор следует вызывать, если универсальный тип имеет ограничение new (Cat, ToyotaTercel), а у фактического типа просто есть конструкторы new (Animal, ToyotaTercel) и new(Cat, Automobile).

Хорошо, давайте попробуем это (что, по моему мнению, похоже на ситуацию, предложенную @supercat)

class Program
{
    static void Main(string[] args)
    {
        Cat cat = new Cat();
        ToyotaTercel toyota = new ToyotaTercel();

        FunnyMethod(cat, toyota);
    }

    public static void FunnyMethod(Animal animal, ToyotaTercel toyota)
    {
        Console.WriteLine("Takes an Animal and a ToyotaTercel");
    }

    public static void FunnyMethod(Cat cat, Automobile car)
    {
        Console.WriteLine("Takes a Cat and an Automobile");
    }
}

public class Automobile
{ }

public class ToyotaTercel : Automobile
{ }

public class Animal
{ }

public class Cat : Animal
{ }

И,вау, он не скомпилируется с ошибкой

Вызов неоднозначен между следующими методами или свойствами: 'TestApp.Program.FunnyMethod (TestApp.Animal, TestApp.ToyotaTercel)' и 'TestApp.Program.FunnyMethod (TestApp.Cat, TestApp.Automobile) '

Я не понимаю, почему результат должен отличаться, если та же проблема возникает из решения с параметризованными ограничениями конструктора, например:

class Program
{
    static void Main(string[] args)
    {
        GenericClass<FunnyClass> gc = new GenericClass<FunnyClass>();
    }
}

public class Automobile
{ }

public class ToyotaTercel : Automobile
{ }

public class Animal
{ }

public class Cat : Animal
{ }

public class FunnyClass
{
    public FunnyClass(Animal animal, ToyotaTercel toyota)
    {            
    }

    public FunnyClass(Cat cat, Automobile car)
    {            
    }
}

public class GenericClass<T>
   where T: new(Cat, ToyotaTercel)
{ }

Теперь, конечно, компилятор не может обработать ограничение на конструкторе, но если это возможно, то почему ошибка не может быть в строке GenericClass<FunnyClass> gc = new GenericClass<FunnyClass>();, аналогичной tТаким образом, при попытке скомпилировать первый пример, пример из FunnyMethod.

В любом случае, я бы пошел еще дальше.Когда кто-то переопределяет абстрактный метод или реализует метод, определенный в интерфейсе, он должен делать это с точно таким же типом параметров, наследники или предки не допускаются.Таким образом, когда требуется параметризованный конструктор, это требование должно выполняться с точным определением, а не с чем-либо еще.В этом случае класс FunnyClass никогда не может быть указан в качестве типа для универсального типа параметра класса GenericClass.

Ответы [ 3 ]

14 голосов
/ 16 марта 2012

цитата Кирка Уолла от меня, конечно, является всем оправданием, которое требуется;мы не обязаны предоставлять обоснование для функций , а не существующих.Функции имеют огромную стоимость.

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

Для начала: рассмотрим более общую функцию.Конструкторы методы .Если вы ожидаете, что найдется способ сказать: «аргумент типа должен иметь конструктор, который принимает int», то почему также не разумно говорить «аргумент типа должен иметь открытый метод с именем Q, который принимает два целых числа и возвращаетstring? "

string M<T>(T t) where T has string Q(int, int)
{
    return t.Q(123, 456);
}

Вам кажется, что это очень общий поступок?Кажется, это противоречит идее обобщений, чтобы иметь такого рода ограничения.

Если эта функция плохая идея для методов, то почему это хорошая идея для методов , котораяслучилось быть конструкторами ?

И наоборот, если это хорошая идея для методов и конструкторов, то зачем останавливаться на достигнутом?

string M<T>(T t) where T has a field named x of type string
{
    return t.x;
}

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

Эта функция, конечно, намного дороже в разработке, реализации, тестировании, документировании и обслуживании.

Второй момент: предположим, что мы решили реализовать эту функцию, либо версию «просто конструкторы», либо версию «любой член». Какой код мы генерируем? Особенность универсального codegen заключается в том, что он был тщательно спроектирован, чтобы вы могли выполнить статический анализ один раз и покончить с ним.Но не существует стандартного способа описания «вызова конструктора, который принимает int» в IL.Нам нужно было бы либо добавить новую концепцию в IL, либо сгенерировать код, чтобы общий вызов конструктора использовал Reflection .

Бывший дорогой;Изменение фундаментальной концепции в IL очень дорого.Последнее (1) медленное, (2) поле параметра и (3) код, который вы могли бы написать сами.Если вы собираетесь использовать отражение, чтобы найти конструктор и вызвать его, тогда напишите код, который использует отражение, чтобы найти конструктор и вызвать его. Если это стратегия code gen, то единственныйПреимущество, которое дает ограничение, состоит в том, что ошибка передачи аргумента типа, который не имеет публичного ctor, который принимает int, обнаруживается во время компиляции, а не во время выполнения .Вы не получаете никаких других преимуществ дженериков, таких как отказ от рефлексии и штрафы за бокс.

4 голосов
/ 02 сентября 2013

Резюме

Это попытка собрать текущую информацию и обходные пути по этому вопросу и представить ее в качестве ответа.

Я считаю, что Generics в сочетании с Constraints является одним из наиболее мощных и элегантных аспектов C # (происходящих из фона шаблонов C ++).where T : Foo замечательно, так как он предоставляет возможности для T, но все еще ограничивает его Foo во время компиляции.Во многих случаях это упростило мою реализацию.Сначала я был немного обеспокоен тем, что использование универсальных типов таким образом может привести к росту обобщенных типов в коде, но я позволил это сделать, и преимущества значительно перевесили любые недостатки.Тем не менее, абстракция всегда падает, когда речь заходит о создании универсального типа, который принимает параметр.

Проблема

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

public class Foo<T>
   where T : new()
{
    public void SomeOperation()
    {
        T something = new T();
        ...
    }
}

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

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

Они все закрыты как "Выиграл"'t Fix' или 'By Design'.Грустная вещь об этом - то, что проблема тогда заблокирована, и больше нет возможности голосовать за них.Однако вы можете проголосовать здесь за конструктор.

Обходные пути

Существует три основных типа обходных путей, ни один из которых не является идеальным:-

  1. Использовать фабрики .Это требует большого количества стандартного кода и служебных данных
  2. Использовать Activator.CreateInstance (typeof (T), arg0, arg1, arg2, ...) .Это мой наименее любимый вариант, поскольку безопасность типов теряется.Что если в дальнейшем вы добавите параметр в конструктор типа T?Вы получаете исключение времени выполнения.
  3. Используйте подход Функция / действие. и здесь .Это мой фаворит, так как он сохраняет безопасность типов и требует меньше стандартного кода.Тем не менее, он по-прежнему не так прост, как new T (a, b, c), и поскольку общая абстракция часто охватывает многие классы, класс, который знает тип, часто находится на расстоянии нескольких классов от класса, которому необходимо создать его экземпляр, чтобыФункция func передается, что приводит к ненужному коду.

Пояснения

В Microsoft Connect предоставляется стандартный ответ:

"Спасибо за ваше предложение. Корпорация Майкрософт получила ряд предложений об изменениях семантики ограничений универсальных типов, а также о своей работе в этой области. Однако в настоящее время Microsoft не может дать никаких обязательств относительно изменений в этомобласть будет частью будущего выпуска продукта. Ваше предложение будет учтено для принятия решений в этой области. Тем временем приведенный ниже пример кода ... "

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

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

Я такженедавно заметил, что в этой ссылке Мадса Торгерсена есть подробности и подробности (см. «Автор: Microsoft, 31.03.2009, 15:29»).

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

Предложения

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

  1. Существует большое значение в отлове багов во время компиляции, а не выполнение (типа безопасность в этомдело).
  2. Кажется, есть и другие варианты, которые не так страшны.Было несколько предложений о том, как это можно реализовать.Примечательно, что Джон Скит предложил «статические интерфейсы» как способ решения этой проблемы, и похоже, что явные ограничения членов уже существуют в CLR, но не в C #, см. Комментарии здесь и обсуждение здесь.Кроме того, комментарий kvb в ответе Эрика Липперта о произвольных ограничениях членов.

Статус

Насколько я могу судить, это не произойдет ни в какой форме.

1 голос
/ 17 марта 2012

Если кто-то хочет иметь метод с универсальным типом T, экземпляры которого могут быть созданы с использованием одного параметра int, он должен иметь метод accept, в дополнение к типу T, либо Func<int, T> или же соответствующий интерфейс, возможно, использующий что-то вроде:

static class IFactoryProducing<ResultType>
{
    interface WithParam<PT1>
    {
        ResultType Create(PT1 p1);
    }
    interface WithParam<PT1,PT2>
    {
        ResultType Create(PT1 p1, PT2 p2);
    }
}

(код выглядел бы лучше, если бы внешний статический класс мог быть объявлен как интерфейс, но IFactoryProducing<T>.WithParam<int> кажется более понятным, чем IFactory<int,T> (поскольку последний неоднозначен относительно того, какой тип является параметром, а какой - результатом) .

В любом случае, когда кто-то передает тип aroud T, он также обходит подходящий фабричный делегат или интерфейс, можно достичь 99% того, чего можно достичь с помощью параметризованных ограничений конструктора. Затраты времени выполнения можно минимизировать, если каждый конструируемый тип генерирует статический экземпляр фабрики, поэтому нет необходимости создавать экземпляры фабрики в каком-либо циклическом контексте.

Кстати, помимо стоимости этой функции, почти наверняка будут существенные ограничения, которые сделают ее менее универсальной, чем обходной путь. Если ограничения конструктора не противоречивы в отношении типов параметров, может потребоваться передать параметр типа для точного типа, требуемого для ограничения конструктора, в дополнение к фактическому типу используемого параметра; к тому времени, когда кто-то это делает, он может сойти с завода. Если они противоречивы, то возникает проблема с разрешением того, какой конструктор следует вызывать, если универсальный тип имеет ограничение new(Cat, ToyotaTercel), а фактический тип просто имеет конструкторы new(Animal, ToyotaTercel) и new(Cat, Automobile).

PS - Чтобы прояснить проблему, контравариантные ограничения конструктора приводят к изменению проблемы «двойного алмаза». Рассмотрим:

T CreateUsingAnimalAutomobile<T>() where T:IThing,new(Animal,Automobile)
{ ... }

T CreateUsingAnimalToyotaTercel<T>() where T:IThing,new(Animal,ToyotaTercel)
{ return CreateUsingAnimalAutomobile<T>(); }

T CreateUsingCatAutomobile<T>() where T:IThing,new(Cat,Automobile)
{ return CreateUsingAnimalAutomobile<T>(); }

IThing thing1=CreateUsingAnimalToyotaTercel<FunnyClass>(); // FunnyClass defined in question
IThing thing2=CreateUsingCatAutomobile<FunnyClass>(); // FunnyClass defined in question

При обработке вызова CreateUsingAnimalToyotaTercel<FunnyClass>() конструктор "Animal, ToyotaTercel" должен удовлетворять ограничению для этого метода, а универсальный тип для этого метода должен удовлетворять ограничению для CreateUsingAnimalAutomobile<T>(). При обработке вызова CreateUsingCatAutomobile<FunnyClass>() конструктор "Cat, Automobile" должен удовлетворять ограничению для этого метода, а универсальный тип для этого метода должен удовлетворять ограничению для CreateUsingAnimalAutomobile<T>().

Проблема в том, что оба вызова будут вызывать вызов одного и того же CreateUsingAnimalAutomobile<SillyClass>() метода, и у этого метода нет способа узнать, какой конструктор должен быть вызван. Неопределенности, связанные с контравариантностью, не являются уникальными для конструкторов, но в большинстве случаев они разрешаются посредством привязки во время компиляции.

...