Utworzyć wystąpienie typu ogólnego, którego konstruktor wymaga parametru?

230

Jeśli BaseFruitma konstruktora, który akceptuje int weight, czy mogę utworzyć egzemplarz owocu za pomocą ogólnej metody takiej jak ta?

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

Przykład został dodany za komentarzami. Wydaje się, że mogę to zrobić tylko wtedy, gdy podam BaseFruitkonstruktor bez parametrów, a następnie wypełnię wszystko przez zmienne składowe. W moim prawdziwym kodzie (nie o owocach) jest to raczej niepraktyczne.

-Aktualizacja-
Wygląda więc na to, że nie można w żaden sposób rozwiązać ograniczeń. Z odpowiedzi istnieją trzy kandydujące rozwiązania:

  • Wzór fabryczny
  • Odbicie
  • Aktywator

Wydaje mi się, że odbicie jest najmniej czyste, ale nie mogę się zdecydować między pozostałymi dwoma.

Boris Callens
źródło
1
BTW: dzisiaj prawdopodobnie rozwiązałbym to przy pomocy wybranej biblioteki IoC.
Boris Callens,
Odbicie i Aktywator są ściśle powiązane.
Rob Vermeulen

Odpowiedzi:

335

Dodatkowo prostszy przykład:

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

Zauważ, że użycie nowego ograniczenia () na T służy tylko do sprawdzania przez kompilator publicznego konstruktora bez parametrów w czasie kompilacji, rzeczywistym kodem użytym do utworzenia typu jest klasa Activator.

Musisz upewnić się co do konkretnego konstruktora, a tego rodzaju wymaganiem może być zapach kodu (a raczej coś, czego powinieneś po prostu unikać w obecnej wersji na c #).

meandmycode
źródło
Ponieważ ten konstruktor jest w klasie podstawowej (BaseFruit), wiem, że będzie miał konstruktor. Ale rzeczywiście, jeśli któregoś dnia zdecyduję, że owoc podstawowy potrzebuje więcej parametrów, mógłbym zostać wkręcony. Jednak zajrzy do klasy ACtivator. Nie słyszałem o tym wcześniej.
Boris Callens,
3
Ten zadziałał dobrze. Istnieje również procedura CreateInstance <T> (), ale nie ma przeciążenia dla parametrów dla jakiegoś powodu.
Boris Callens
20
Nie ma potrzeby używania new object[] { weight }. CreateInstancejest zadeklarowany z parametrami public static object CreateInstance(Type type, params object[] args), więc możesz to zrobić return (T) Activator.CreateInstance(typeof(T), weight);. Jeśli istnieje wiele parametrów, przekaż je jako osobne argumenty. Tylko jeśli masz już konstruowaną listę parametrów, powinieneś zadać sobie trud, aby ją przekonwertować object[]i przekazać do CreateInstance.
ErikE
2
To będzie miało problemy z wydajnością, które przeczytałem. Zamiast tego użyj skompilowanej lambdy. vagifabilov.wordpress.com/2010/04/02/…
David
1
@RobVermeulen - Myślę, że coś w rodzaju właściwości statycznej dla każdej klasy Fruit, która zawiera właściwość, Funcktóra tworzy nową instancję. Załóżmy, że Appleużycie konstruktora jest new Apple(wgt). Następnie dodaj do Appleklasy tę definicję: static Func<float, Fruit> CreateOne { get; } = (wgt) => new Apple(wgt);W ustawieniu fabrycznym public static Fruit CreateFruitGiven(float weight, Func<float, Fruit> createOne) { return createOne(weight); } Użycie: Factory.CreateFruit(57.3f, Apple.CreateOne);- który tworzy i zwraca Apple, z weight=57.3f.
ToolmakerSteve
92

Nie można użyć żadnego sparametryzowanego konstruktora. Możesz użyć konstruktora bez parametrów, jeśli masz where T : new()ograniczenie.

To ból, ale takie jest życie :(

Jest to jedna z rzeczy, które chciałbym rozwiązać za pomocą „interfejsów statycznych” . Będziesz wtedy mógł ograniczyć T do włączenia metod statycznych, operatorów i konstruktorów, a następnie wywołać je.

Jon Skeet
źródło
2
Przynajmniej MOŻESZ zrobić takie ograniczenia - Java zawsze mnie rozczarowuje.
Marcel Jackwerth,
@JonSkeet: Jeśli odsłoniłem interfejs API za pomocą .NET generic, aby można go było wywołać w VB6.0..Czy nadal jest wykonalny?
Roy Lee
@Roylee: Nie mam pojęcia, ale podejrzewam, że nie.
Jon Skeet,
Sądzę, że kompilator języka mógłby dodać statyczne interfejsy bez zmian w środowisku wykonawczym, choć dobrze byłoby, gdyby zespoły językowe koordynowały dane. Określ, że każda klasa, która twierdzi, że implementuje interfejs statyczny, musi zawierać klasę zagnieżdżoną o określonej nazwie związanej z interfejsem, która definiuje instancję statycznego singletonu własnego typu. Z interfejsem skojarzony byłby statyczny typ ogólny z polem instancji, który musiałby zostać załadowany singletonem jeden raz przez Reflection, ale mógłby być użyty bezpośrednio po tym.
supercat
Sparametryzowane ograniczenie konstruktora może być obsługiwane w ten sam sposób (przy użyciu metody fabrycznej i parametru ogólnego dla jego typu zwracanego); w żadnym wypadku nic nie zapobiegłoby, aby kod napisany w języku, który nie obsługiwał takiej funkcji, twierdził, że implementuje interfejs bez zdefiniowania odpowiedniego typu statycznego, więc kod napisany przy użyciu takich języków może zawieść w czasie wykonywania, ale użytkownik może uniknąć refleksji kod.
supercat
61

Tak; zmień swoje miejsce pobytu:

where T:BaseFruit, new()

Działa to jednak tylko z konstruktorami bez parametrów . Będziesz musiał mieć inne sposoby na ustawienie swojej właściwości (ustawienie samej nieruchomości lub czegoś podobnego).

Adam Robinson
źródło
Jeśli konstruktor nie ma parametrów, wydaje mi się to bezpieczne.
PerpetualStudent
Uratowałeś mi życie. Nie udało mi się ograniczyć T do słowa kluczowego class i new ().
Genotypek
28

Najprostsze rozwiązanie Activator.CreateInstance<T>()

użytkownik1471935
źródło
1
Dzięki za sugestię, doprowadziło mnie to tam, gdzie powinienem być. Chociaż nie pozwala to na użycie sparametryzowanego konstruktora. Możesz jednak użyć nieogólnego wariantu: Activator.CreateInstance (typeof (T), nowy obiekt [] {...}), w którym tablica obiektów zawiera argumenty dla konstruktora.
Rob Vermeulen,
19

Jak zauważył Jon, jest to życie w ograniczaniu nieparametrycznego konstruktora. Jednak innym rozwiązaniem jest użycie wzorca fabrycznego. Można to łatwo ograniczyć

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);
}

Jeszcze inną opcją jest zastosowanie podejścia funkcjonalnego. Przekaż metodę fabryczną.

public void AddFruit<T>(Func<int,T> factoryDel) where T : BaseFruit { 
  BaseFruit fruit = factoryDel(weight); /* new Apple(150); */
  fruit.Enlist(fruitManager);
}
JaredPar
źródło
2
Dobra sugestia - chociaż jeśli nie jesteś ostrożny, możesz skończyć w piekle interfejsu API Java DOM, z mnóstwem fabryk :(
Jon Skeet
Tak, to jest rozwiązanie, które sam sobie wyobrażałem. Ale liczyłem na coś w rodzaju ograniczeń. Nie sądzę więc ...
Boris Callens,
@boris, niestety poszukiwany język ograniczeń nie istnieje w tym momencie
JaredPar
11

Możesz to zrobić za pomocą odbicia:

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);
}

EDYCJA: Dodano konstruktor == kontrola zerowa.

EDYCJA: Szybszy wariant wykorzystujący pamięć podręczną:

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) });
}
mmmmmmmm
źródło
Chociaż nie podoba mi się refleksja, jak wyjaśnili inni, tak właśnie jest obecnie. Widząc, jak ten konstruktor nie będzie nazwać za dużo, mógłbym to zrobić. Lub fabryka. Jeszcze nie wiem
Boris Callens,
Jest to obecnie moje preferowane podejście, ponieważ nie dodaje większej złożoności po stronie wywołania.
Rob Vermeulen,
Ale teraz czytałem o sugestii Aktywatora, która ma podobną nieprzyjemność jak powyższe rozwiązanie odbicia, ale z mniejszą liczbą linii kodu :) Idę do opcji Aktywatora.
Rob Vermeulen,
1

Jako dodatek do sugestii użytkownika1471935:

Aby utworzyć instancję klasy ogólnej za pomocą konstruktora z co najmniej jednym parametrem, możesz teraz użyć klasy Activator.

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

Lista obiektów to parametry, które chcesz podać. Według Microsoft :

CreateInstance [...] tworzy instancję określonego typu za pomocą konstruktora, który najlepiej pasuje do określonych parametrów.

Istnieje również ogólna wersja CreateInstance ( CreateInstance<T>()), ale ta również nie pozwala na podanie parametrów konstruktora.

Rob Vermeulen
źródło
1

Stworzyłem tę metodę:

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;
}

Używam tego w ten sposób:

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

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

Kod:

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

B instanceB = new B();
instanceB = ConvertParentObjToChildObj<A,B>(instanceA);
Diocleziano Carletti
źródło
0

Ostatnio natknąłem się na bardzo podobny problem. Chciałem tylko podzielić się z Wami naszym rozwiązaniem. Chciałem utworzyć instancję obiektu Car<CarA>z obiektu Json, który miał wyliczenie:

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) });
ostry
źródło
-2

Nadal jest to możliwe, przy wysokiej wydajności, wykonując następujące czynności:

    //
    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;
    }

i

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

Odpowiednie klasy muszą następnie wyprowadzić się z tego interfejsu i odpowiednio zainicjować. Proszę zauważyć, że w moim przypadku ten kod jest częścią otaczającej klasy, która ma już <T> jako parametr ogólny. R w moim przypadku jest także klasą tylko do odczytu. IMO, publiczna dostępność funkcji Initialize () nie ma negatywnego wpływu na niezmienność. Użytkownik tej klasy może umieścić inny obiekt, ale nie zmodyfikuje to podstawowej kolekcji.

cskwg
źródło