Створити екземпляр загального типу, конструктору якого потрібен параметр?


230

Якщо BaseFruitв конструкторі, який приймає int weight, можна створити шматочок фрукта таким загальним методом?

public void AddFruit<T>()where T: BaseFruit{
    BaseFruit fruit = new T(weight); /*new Apple(150);*/
    fruit.Enlist(fruitManager);
}

Приклад додається за коментарями. Здається, я можу це зробити, лише якщо даю BaseFruitконструктор без параметрів, а потім заповнити все за допомогою змінних членів. У моєму реальному коді (не про фрукти) це досить недоцільно.

-Update-
Отож, схоже, це ніяк не може бути вирішене обмеженнями. З відповідей є три рішення кандидата:

  • Фабричний візерунок
  • Рефлексія
  • Активатор

Я схильний вважати, що рефлексія є найменш чистою, але я не можу визначитися між двома іншими.


1
До речі: сьогодні я, мабуть, вирішив це за допомогою обраної бібліотеки IoC.
Борис Калленс

Відбиття та активатор насправді тісно пов'язані.
Роб Вермеулен

Відповіді:


335

Додатково простіший приклад:

return (T)Activator.CreateInstance(typeof(T), new object[] { weight });

Зауважте, що використання нового () обмеження на T полягає лише у тому, щоб компілятор перевіряв відкритий конструктор без параметрів під час компіляції, фактичним кодом, який використовується для створення типу, є клас Активатор.

Вам потрібно буде впевнитись у тому, що існує конкретний конструктор, і ця вимога може мати запах коду (а точніше, чого слід просто намагатися уникати в поточній версії на c #).


Оскільки цей конструктор знаходиться на базовому рівні (BaseFruit), я знаю, що у нього буде конструктор. Але дійсно, якщо одного дня я вирішу, що базовий фрукт потребує більшої кількості параметрів, я міг би бути накручений. Хоча ми заглянемо в клас Активатор. Не чув про це раніше.
Борис Калленс

3
Цей добре працював. Існує також процедура CreateInstance <T> (), але вона не має перевантаження параметрів для деякого расону ..
Boris Callens

20
Не потрібно користуватися new object[] { weight }. CreateInstanceоголошується парами public static object CreateInstance(Type type, params object[] args), так що ви можете просто зробити return (T) Activator.CreateInstance(typeof(T), weight);. Якщо є кілька параметрів, передайте їх як окремі аргументи. Тільки якщо у вас вже є побудована безліч параметрів, вам слід перетворити його на перетворення object[]та передачу CreateInstance.
ErikE

2
У цьому виникнуть проблеми з ефективністю, які я прочитав. Використовуйте замість цього складену лямбда. vagifabilov.wordpress.com/2010/04/02/…
Девід

1
@RobVermeulen - Я думаю, що щось подібне до статичної властивості для кожного класу Fruit, яка містить a, Funcякий створює новий екземпляр. Припустимо, Appleвикористання конструктора new Apple(wgt). Потім додайте до Appleкласу це визначення: static Func<float, Fruit> CreateOne { get; } = (wgt) => new Apple(wgt);У Factory define public static Fruit CreateFruitGiven(float weight, Func<float, Fruit> createOne) { return createOne(weight); } Usage: Factory.CreateFruit(57.3f, Apple.CreateOne);- що створює та повертає Apple, з weight=57.3f.
ToolmakerSteve

92

Ви не можете використовувати будь-який параметризований конструктор. Ви можете використовувати конструктор без параметрів, якщо у вас where T : new()обмеження.

Це біль, але таке життя :(

Це одна з речей, яку я хотів би вирішити зі "статичними інтерфейсами" . Тоді ви зможете обмежити T, щоб включити статичні методи, оператори та конструктори, а потім викликати їх.


2
Принаймні ви МОЖЕТЕ робити такі обмеження - Java завжди мене розчаровує.
Марсель Джекверт

@JonSkeet: Якщо я виявив API з .NET generic, щоб його викликали у VB6.0..Чому він все ще працює?
Рой Лі

@Roylee: Я поняття не маю, але підозрюю, що ні.
Джон Скіт

Я думаю, що статичні інтерфейси можуть бути додані компілятором мови без змін у процесі виконання, хоча було б добре, щоб мовні команди координували деталі. Вкажіть, що кожен клас, який претендує на реалізацію статичного інтерфейсу, повинен містити вкладений клас із конкретним іменем, пов’язаним із інтерфейсом, який визначає статичний однотонний екземпляр власного типу. Асоційований з інтерфейсом буде статичний загальний тип із полем екземпляра, яке потрібно буде завантажувати однократним шляхом за допомогою Reflection, але його можна використовувати безпосередньо після цього.
supercat

Параметризоване обмеження конструктора може оброблятися приблизно однаково (використовуючи заводський метод та загальний параметр для його типу повернення); ні в якому разі нічого не заважає коду, написаному мовою, яка не підтримує таку функцію, вимагати впровадити інтерфейс без визначення належного статичного типу, тому код, написаний такими мовами, може не працювати під час виконання, але відображення можна уникнути у користувача код.
supercat

61

Так; змінити місце, де бути:

where T:BaseFruit, new()

Однак це працює лише з параметричними конструкторами. Вам доведеться мати інші засоби налаштування власності (встановлення власності або щось подібне).


Якщо у конструктора немає параметрів, це здається мені безпечним.
PerpetualStudent

Ти врятував мені життя. Не вдалося обмежити T класом та новим () ключовим словом.
Genotypek

28

Найпростіше рішення Activator.CreateInstance<T>()


1
Дякуємо за пропозицію, вона перевела мене там, де я мав бути. Хоча це не дозволяє використовувати параметризований конструктор. Але ви можете використати негенеріальний варіант: Activator.CreateInstance (typeof (T), новий об’єкт [] {...}), де масив об'єктів містить аргументи для конструктора.
Роб Вермеулен

19

Як зазначав Джон, це життя для обмеження безпараметричного конструктора. Однак іншим рішенням є використання заводського зразка. Це легко обмежити

interface IFruitFactory<T> where T : BaseFruit {
  T Create(int weight);
}

public void AddFruit<T>( IFruitFactory<T> factory ) where T: BaseFruit {    
  BaseFruit fruit = factory.Create(weight); /*new Apple(150);*/    
  fruit.Enlist(fruitManager);
}

Ще один варіант - використовувати функціональний підхід. Передати фабричним методом.

public void AddFruit<T>(Func<int,T> factoryDel) where T : BaseFruit { 
  BaseFruit fruit = factoryDel(weight); /* new Apple(150); */
  fruit.Enlist(fruitManager);
}

2
Хороша пропозиція - хоча якщо ви не обережні, ви можете опинитися в пеклі API Java DOM, з фабриками багато :(
Jon Skeet

Так, це рішення, про яке я сам думав. Але я сподівався на щось у межах обмежень. Вгадайте не тоді ..
Борис Калленс

@boris, на жаль, мовою обмеження, яку ви шукаєте, на даний момент не існує
JaredPar

11

Ви можете зробити це за допомогою рефлексії:

public void AddFruit<T>()where T: BaseFruit
{
  ConstructorInfo constructor = typeof(T).GetConstructor(new Type[] { typeof(int) });
  if (constructor == null)
  {
    throw new InvalidOperationException("Type " + typeof(T).Name + " does not contain an appropriate constructor");
  }
  BaseFruit fruit = constructor.Invoke(new object[] { (int)150 }) as BaseFruit;
  fruit.Enlist(fruitManager);
}

EDIT: Доданий конструктор == null check.

EDIT: Більш швидкий варіант використання кешу:

public void AddFruit<T>()where T: BaseFruit
{
  var constructor = FruitCompany<T>.constructor;
  if (constructor == null)
  {
    throw new InvalidOperationException("Type " + typeof(T).Name + " does not contain an appropriate constructor");
  }
  var fruit = constructor.Invoke(new object[] { (int)150 }) as BaseFruit;
  fruit.Enlist(fruitManager);
}
private static class FruitCompany<T>
{
  public static readonly ConstructorInfo constructor = typeof(T).GetConstructor(new Type[] { typeof(int) });
}

Хоча мені не подобається накладні роздуми, як пояснили інші, це просто так, як це є зараз. Бачачи, як цей конструктор не буде називатися занадто багато, я міг би піти з цим. Або фабрика. Ще не знаю.
Борис Калленс

Наразі це мій переважний підхід, оскільки він не додає більшої складності на стороні виклику.
Роб Вермеулен

Але зараз я прочитав про пропозицію Activator, яка має схожу гнучкість, як і вищезазначене рішення для відображення, але з меншим рядком коду :) Я збираюся піти на варіант активатора.
Роб Вермеулен

1

Як додаток до пропозиції користувача1471935:

Для створення загального класу за допомогою конструктора з одним або кількома параметрами тепер можна використовувати клас Активатор.

T instance = Activator.CreateInstance(typeof(T), new object[] {...}) 

Список об'єктів - це параметри, які потрібно надати. За даними Microsoft :

CreateInstance [...] створює екземпляр зазначеного типу за допомогою конструктора, який найкраще відповідає заданим параметрам.

Існує також загальна версія CreateInstance ( CreateInstance<T>()), але вона також не дозволяє постачати параметри конструктора.


1

Я створив цей метод:

public static V ConvertParentObjToChildObj<T,V> (T obj) where V : new()
{
    Type typeT = typeof(T);
    PropertyInfo[] propertiesT = typeT.GetProperties();
    V newV = new V();
    foreach (var propT in propertiesT)
    {
        var nomePropT = propT.Name;
        var valuePropT = propT.GetValue(obj, null);

        Type typeV = typeof(V);
        PropertyInfo[] propertiesV = typeV.GetProperties();
        foreach (var propV in propertiesV)
        {
            var nomePropV = propV.Name;
            if(nomePropT == nomePropV)
            {
                propV.SetValue(newV, valuePropT);
                break;
            }
        }
    }
    return newV;
}

Я використовую це таким чином:

public class A 
{
    public int PROP1 {get; set;}
}

public class B : A
{
    public int PROP2 {get; set;}
}

Код:

A instanceA = new A();
instanceA.PROP1 = 1;

B instanceB = new B();
instanceB = ConvertParentObjToChildObj<A,B>(instanceA);

0

Нещодавно я зіткнувся з дуже подібною проблемою. Просто хотів поділитися нашим рішенням з усіма вами. Я хотів, щоб я створив екземпляр Car<CarA>об'єкта json, за допомогою якого був enum:

Dictionary<MyEnum, Type> mapper = new Dictionary<MyEnum, Type>();

mapper.Add(1, typeof(CarA));
mapper.Add(2, typeof(BarB)); 

public class Car<T> where T : class
{       
    public T Detail { get; set; }
    public Car(T data)
    {
       Detail = data;
    }
}
public class CarA
{  
    public int PropA { get; set; }
    public CarA(){}
}
public class CarB
{
    public int PropB { get; set; }
    public CarB(){}
}

var jsonObj = {"Type":"1","PropA":"10"}
MyEnum t = GetTypeOfCar(jsonObj);
Type objectT = mapper[t]
Type genericType = typeof(Car<>);
Type carTypeWithGenerics = genericType.MakeGenericType(objectT);
Activator.CreateInstance(carTypeWithGenerics , new Object[] { JsonConvert.DeserializeObject(jsonObj, objectT) });

-2

З високою продуктивністю все ще можна виконати наступні дії:

    //
    public List<R> GetAllItems<R>() where R : IBaseRO, new() {
        var list = new List<R>();
        using ( var wl = new ReaderLock<T>( this ) ) {
            foreach ( var bo in this.items ) {
                T t = bo.Value.Data as T;
                R r = new R();
                r.Initialize( t );
                list.Add( r );
            }
        }
        return list;
    }

і

    //
///<summary>Base class for read-only objects</summary>
public partial interface IBaseRO  {
    void Initialize( IDTO dto );
    void Initialize( object value );
}

Відповідні класи повинні виходити з цього інтерфейсу та відповідно ініціалізуватися. Зауважте, що в моєму випадку цей код є частиною оточуючого класу, який вже має <T> як загальний параметр. R, в моєму випадку, також є класом, доступним лише для читання. ІМО, загальнодоступність функцій Initialize () не має негативного впливу на незмінність. Користувач цього класу міг би поставити інший об’єкт, але це не змінює базову колекцію.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.