Ограничение параметров типового типа, чтобы иметь конкретный конструктор
Я хотел бы знать, почему новое ограничение для параметра общего типа может применяться только без параметров, т.е. можно ограничить тип конструктором без параметров, но нельзя ограничить класс, чтобы, скажем, конструктор, который получает int как параметр. Я знаю об этом, используя рефлексию или шаблон factory, который работает нормально, хорошо. Но я действительно хотел бы знать, почему, потому что я думал об этом, и я действительно не могу представить разницу между конструктором без параметров и параметрами, которые оправдывали бы это ограничение на новом ограничении. Что мне не хватает?
Большое спасибо
Аргумент 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 пытается указать некоторые примеры как (цитата из комментария)
Также было бы трудно точно определить, как должны работать конструкторские ограничения, не приводя к неожиданному поведению.
но я действительно должен не согласиться. В С# (ну, в .NET) удивляет, как "Как сделать пингвиновую муху?" просто не бывает. Есть довольно простые правила относительно того, как компилятор разрешает вызовы методов, и если компилятор не может его решить, ну, он не пройдет, не будет компилироваться.
Последний пример:
Если они контравариантны, тогда возникает проблема, решающая, какой конструктор следует вызывать, если общий тип имеет ограничение new (Cat, ToyotaTercel), а у фактического типа есть только конструкторы new (Animal, ToyotaTercel) и new (Cat, Автомобили).
Ну, давайте попробуем это (что, на мой взгляд, похоже на ситуацию, предложенную @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>();
, подобной полученной при попытке скомпилировать первый пример, а <<28 > .
Во всяком случае, я бы сделал еще один шаг. Когда кто-то переопределяет абстрактный метод или реализует метод, определенный на интерфейсе, требуется сделать это с точно таким же типом параметров, что и наследники или предки не разрешены. Поэтому, когда требуется параметризованный конструктор, требование должно выполняться с точным определением, а не с чем-либо еще. В этом случае класс FunnyClass
никогда не может быть указан как тип для общего типа параметров класса GenericClass
.
Ответы
Ответ 1
Цитата Кирка Волла от меня, конечно, все оправдание, которое требуется; мы не обязаны предоставлять обоснование отсутствующим функциям. Особенности имеют огромные затраты.
Однако в этом конкретном случае я могу, конечно, дать вам несколько причин, по которым я хотел бы вернуться к этой функции, если бы она возникла на проектной встрече как возможная функция для будущей версии языка.
Для начала: рассмотрим более общую функцию. Конструкторы - это методы. Если вы ожидаете, что есть способ сказать: "аргумент типа должен иметь конструктор, который принимает int", то почему также неразумно говорить "аргумент типа должен иметь открытый метод с именем Q, который принимает два целых числа и возвращает строка?"
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;
}
Я говорю, что мы должны либо выполнять всю функцию, либо вообще не делать этого. Если важно иметь возможность ограничить типы конкретными конструкторами, то пусть все функции и ограничивать их можно использовать на основе элементов в целом, а не только для конструкторов.
Эта функция, конечно, намного дороже в разработке, внедрении, тестировании, документировании и обслуживании.
Вторая точка: предположим, что мы решили реализовать эту функцию, либо версию "просто конструкторы", либо версию "любой член". Какой код мы генерируем? Что касается универсального кодегана, так это то, что он был тщательно разработан так, что вы можете сделать статический анализ один раз и сделать с ним. Но нет стандартного способа описать "вызов конструктора, который принимает int" в IL. Нам пришлось бы либо добавить новую концепцию в IL, либо сгенерировать код, чтобы общий вызов конструктора использовал Reflection.
Первый дорогой; изменение фундаментальной концепции в ИЛ очень дорого. Последний (1) медленный, (2) вводит параметр, а (3) - код, который вы могли бы написать сами. Если вы собираетесь использовать отражение, чтобы найти конструктор и вызвать его, напишите код, который использует отражение, чтобы найти конструктор и вызвать его. Если это стратегия генерации кода, то единственное преимущество, которое дает ограничение, заключается в том, что ошибка передачи аргумента типа, которая не имеет общедоступного ctor, который принимает int, попадает во время компиляции, а не во время выполнения. Вы не получаете каких-либо других преимуществ дженериков, например, избегаете отражения и штрафов за бокс.
Ответ 2
Резюме
Это попытка зафиксировать текущую информацию и обходные пути по этому вопросу и представить ее в качестве ответа.
Я нахожу Generics в сочетании с Constraints одним из самых мощных и элегантных аспектов С# (исходя из фона шаблонов С++). where T : Foo
отлично, поскольку он вводит возможности T, но при этом сдерживает его в Foo во время компиляции. Во многих случаях это упростило мою реализацию. Во-первых, меня немного беспокоило то, что использование общих типов таким образом может привести к генерированию дженериков через код, но я разрешил это сделать, и преимущества значительно перевешивают любые недостатки. Однако абстракция всегда падает, когда дело доходит до построения типа Generic, который принимает параметр.
Проблема
При ограничении общего класса вы можете указать, что общий тип должен иметь конструктор без параметров и затем его экземпляр:
public class Foo<T>
where T : new()
{
public void SomeOperation()
{
T something = new T();
...
}
}
Проблема заключается в том, что вы умеете ограничивать беззаметные конструкторы. Это означает, что для конструкторов, имеющих параметры, необходимо использовать один из предложенных ниже обходных путей. Как описано ниже, обходные решения имеют недостатки, начиная от необходимости введения дополнительного кода в очень опасное состояние. Кроме того, если у меня есть класс, который имеет открытый конструктор без параметров, который используется универсальным методом, но где-то вниз по треку, этот класс изменяется, так что у конструктора теперь есть параметр, тогда мне нужно изменить дизайн шаблона и окружения кода для использования одного из обходных решений, а не new().
Это то, что Microsoft определенно знает, см. эти ссылки в Microsoft Connect как образец (не считая запутанных пользователей Stack Overflow, задающих вопрос) здесь здесь здесь здесь здесь здесь здесь.
Все они закрыты как "Не исправить" или "По дизайну". Печально то, что проблема закрывается, и их уже невозможно проголосовать. Однако вы можете проголосовать здесь для функции конструктора.
Обходные пути
Существует три основных типа обходных путей, ни одна из которых не идеальна: -
- Использовать фабрики. Это требует большого количества кода шаблона и служебных данных
- Используйте Activator.CreateInstance(typeof (T), arg0, arg1, arg2,...). Это мой наименее любимый, поскольку безопасность типов теряется. Что делать, если на дорожке вы добавляете параметр к конструктору типа T? Вы получаете исключение во время выполнения.
- Используйте подход Function/action. и здесь. Это мой любимый вариант, поскольку он сохраняет безопасность типов и требует меньшего количества шаблонов. Тем не менее, это все еще не так просто, как новые T (a, b, c), и как общая абстракция часто охватывает много классов, класс, который знает этот тип, часто находится в нескольких классах от класса, который должен создать его, чтобы func передается, что приводит к ненужному коду.
Объяснения
Стандартный ответ предоставляется в Microsoft Connect, который:
"Спасибо за ваше предложение. Microsoft получила ряд предложения об изменениях семантики ограничений общих типов, а также выполняет свою собственную работу в этой области. Однако в это время Microsoft не может дать каких-либо обязательств относительно того, что изменения в этой области будут быть частью будущего выпуска продукта. Ваше предложение будет отмечено помогут принять решения в этой области. Тем временем пример кода ниже..."
Обходной путь на самом деле не является моим рекомендуемым решением обо всех вариантах, поскольку он не безопасен для типов и приводит к исключению среды выполнения, если вам когда-либо приходилось добавлять еще один параметр в конструктор.
Лучшее объяснение, которое я могу найти, предлагает Эрик Липперт в этом самом столбце.
Я очень благодарен за этот ответ, но я думаю, что дальнейшая дискуссия требуется на этом уровне пользователя, а затем на техническом уровне (вероятно, людьми, которые знают больше, чем я о внутренних).
Недавно я также заметил, что в этой ссылке есть хорошая и подробная информация от Mads Torgersen (см. "Отправлено Microsoft на 3/31/2009 в 3:29 вечера" ).
Проблема заключается в том, что конструкторы отличаются от других методов тем, что мы уже можем ограничивать методы настолько, насколько нам нужно, с помощью ограничений деривации (интерфейса или базового класса). Могут быть некоторые случаи, когда ограничения метода полезны, я их никогда не нуждался, однако я постоянно сталкивался с ограничением конструктора без параметров. Разумеется, идеальное решение (а не только для конструктора) было бы идеальным, и Microsoft тоже должна была решить это.
Предложения
Что касается дискуссионных выгод против сложности реализации, я могу оценить это, но хотел бы сделать следующее: -
- При сбое ошибок во время компиляции существует большая ценность, а не время выполнения (в этом случае, например, введите безопасность).
- Кажется, есть другие варианты, которые не так уж ужасны. Было высказано несколько предложений о том, как это можно реализовать. Примечательно, что Джон Скит предложил "статические интерфейсы" как способ решить эту проблему, и кажется, что в CLR уже существуют явные ограничения членов, но не на С#, см. Комментарии здесь и обсуждение здесь. Кроме того, комментарий kvb в ответе Эрика Липперта о произвольных ограничениях члена.
Статус
Невозможно произойти в любой форме формы, насколько я могу судить.
Ответ 3
Если требуется иметь метод с общим типом T
, экземпляры которого могут быть созданы с использованием единственного параметра int
, необходимо, чтобы метод принял, помимо типа T
, либо a 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 type T
, он также проходит вокруг подходящего делегата или интерфейса factory, можно достичь 99% того, что можно было бы достичь с параметризованными ограничениями конструктора. Затраты времени выполнения можно свести к минимуму за счет того, что каждый построенный тип генерирует статический экземпляр factory, поэтому нет необходимости создавать экземпляры factory в любом виде цикла.
Кстати, помимо стоимости этой функции, почти наверняка будут некоторые существенные ограничения, которые сделают ее менее универсальной, чем обходной путь. Если ограничения конструктора не являются контравариантными по отношению к типам параметров, может потребоваться пройти вокруг параметра типа для точного типа, необходимого для ограничения конструктора, в дополнение к фактическому типу параметра, который будет использоваться; к тому времени, когда это делается, можно было бы обойти factory. Если они контравариантны, тогда возникает проблема, решающая, какой конструктор следует вызывать, если общий тип имеет ограничение 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>()
, и этот метод не знает, какой конструктор должен быть вызван. Сознания, связанные с контравариантностью, не уникальны для конструкторов, но в большинстве случаев они разрешаются посредством привязки времени компиляции.
Ответ 4
О конструкторах.
Думать об этом. Вы не можете создавать экземпляр только с типом интерфейса в руке.
Вы используете интерфейс, чтобы указать из экземпляра объекта, что экземпляр имеет определенные функции. Вы не можете вызвать экземпляр ctor:
public interface IInterface
{
void SomeMethod();
}
public class Example1 : IInterface
{
public void SomeMethod() { }
}
public class Example2 : IInterface
{
public void SomeMethod() { }
}
public class InterfaceConsumer
{
private IInterface _myService;
public InterfaceConsumer(IInterface service)
{
_myService = service;
}
}
Вы не можете вызвать _myService.new()
или _myService.ctor()
.