Buat instance tipe generik yang konstruktornya memerlukan parameter?

230

Jika BaseFruitmemiliki konstruktor yang menerima int weight, bisakah saya instantiate sepotong buah dengan metode generik seperti ini?

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

Contoh ditambahkan di balik komentar. Sepertinya saya hanya dapat melakukan ini jika saya memberikan BaseFruitkonstruktor tanpa parameter dan kemudian mengisi semuanya melalui variabel anggota. Dalam kode asli saya (bukan tentang buah) ini agak tidak praktis.

-Perbaharui-
Jadi sepertinya itu tidak bisa diselesaikan dengan kendala dengan cara apa pun. Dari jawaban ada tiga solusi kandidat:

  • Pola Pabrik
  • Refleksi
  • Penggerak

Saya cenderung berpikir refleksi adalah yang paling bersih, tetapi saya tidak bisa memutuskan di antara keduanya.

Boris Callens
sumber
1
BTW: hari ini saya mungkin akan menyelesaikan ini dengan perpustakaan pilihan IoC.
Boris Callens
Refleksi dan Aktivator sebenarnya terkait erat.
Rob Vermeulen

Jawaban:

335

Selain itu contoh yang lebih sederhana:

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

Perhatikan bahwa menggunakan batasan baru () pada T hanya untuk membuat pemeriksaan kompiler untuk konstruktor tanpa parameter publik pada waktu kompilasi, kode aktual yang digunakan untuk membuat tipe adalah kelas Activator.

Anda perlu memastikan diri Anda mengenai konstruktor spesifik yang ada, dan persyaratan semacam ini mungkin berupa bau kode (atau lebih tepatnya sesuatu yang harus Anda coba hindari dalam versi saat ini di c #).

meandmycode
sumber
Karena konstruktor ini berada di baseclass (BaseFruit) saya tahu itu akan memiliki konstruktor. Tetapi memang, jika suatu hari saya memutuskan buah basa membutuhkan lebih banyak parameter, saya bisa kacau. Akan melihat ke kelas ACtivator. Tidak pernah mendengarnya sebelumnya.
Boris Callens
3
Yang ini berhasil dengan baik. Ada juga CreateInstance <T> () prosedur, tetapi yang tidak memiliki kelebihan untuk parameter untuk beberapa rason ..
Boris Callens
20
Tidak perlu digunakan new object[] { weight }. CreateInstancedideklarasikan dengan params public static object CreateInstance(Type type, params object[] args),, jadi Anda bisa melakukannya return (T) Activator.CreateInstance(typeof(T), weight);. Jika ada beberapa parameter, berikan sebagai argumen terpisah. Hanya jika Anda sudah memiliki parameter enumerable yang dibangun Anda harus repot mengubahnya object[]dan meneruskannya ke CreateInstance.
ErikE
2
Ini akan memiliki masalah kinerja yang saya baca. Gunakan lambda yang dikompilasi sebagai gantinya. vagifabilov.wordpress.com/2010/04/02/...
David
1
@RobVermeulen - Saya pikir sesuatu seperti properti statis pada setiap kelas Buah, yang berisi Funcyang menciptakan instance baru. Misalkan Applepenggunaan konstruktor adalah new Apple(wgt). Kemudian tambahkan ke Applekelas definisi ini: static Func<float, Fruit> CreateOne { get; } = (wgt) => new Apple(wgt);Di Factory define public static Fruit CreateFruitGiven(float weight, Func<float, Fruit> createOne) { return createOne(weight); } Usage: Factory.CreateFruit(57.3f, Apple.CreateOne);- yang membuat dan mengembalikan Apple, dengan weight=57.3f.
ToolmakerSteve
92

Anda tidak dapat menggunakan konstruktor parameterised apa pun. Anda dapat menggunakan konstruktor tanpa parameter jika Anda memiliki where T : new()batasan " ".

Ini menyebalkan, tapi begitulah hidup :(

Ini adalah salah satu hal yang ingin saya bahas dengan "antarmuka statis" . Anda kemudian dapat membatasi T untuk memasukkan metode statis, operator dan konstruktor, dan kemudian memanggil mereka.

Jon Skeet
sumber
2
Setidaknya Anda BISA melakukan kendala seperti itu - Java selalu mengecewakan saya.
Marcel Jackwerth
@ JonSkeet: Jika saya mengekspos API dengan .NET generic untuk dipanggil dalam VB6.0..Apakah itu masih bisa diterapkan?
Roy Lee
@ Roylee: Saya tidak tahu, tapi saya kira tidak.
Jon Skeet
Saya akan berpikir antarmuka statis dapat ditambahkan oleh kompiler bahasa tanpa perubahan ke runtime, meskipun akan lebih baik untuk memiliki tim bahasa yang berkoordinasi pada rincian. Tentukan bahwa setiap kelas yang mengklaim untuk mengimplementasikan antarmuka statis harus berisi kelas bersarang dengan nama terkait antarmuka tertentu, yang mendefinisikan instance singleton statis dari tipenya sendiri. Terkait dengan antarmuka akan menjadi tipe generik statis dengan bidang contoh yang perlu dimuat dengan singleton sekali melalui Refleksi, tetapi dapat digunakan langsung setelah itu.
supercat
Batasan konstruktor berparameter dapat ditangani dengan cara yang sama (menggunakan metode pabrik, dan parameter umum untuk jenis pengembaliannya); dalam kedua kasus tidak ada yang mencegah kode yang ditulis dalam bahasa yang tidak mendukung fitur seperti itu dari klaim untuk mengimplementasikan antarmuka tanpa mendefinisikan jenis statis yang tepat, sehingga kode yang ditulis menggunakan bahasa tersebut dapat gagal saat runtime, tetapi Refleksi dapat dihindari di pengguna kode.
supercat
61

Iya; ubah tempat Anda menjadi:

where T:BaseFruit, new()

Namun, ini hanya berfungsi dengan konstruktor tanpa parameter . Anda harus memiliki cara lain untuk menyetel properti Anda (menyetel properti itu sendiri atau yang serupa).

Adam Robinson
sumber
Jika konstruktor tidak memiliki parameter ini sepertinya aman untuk saya.
PerpetualStudent
Anda telah menyelamatkan hidup saya. Saya tidak dapat mengatur untuk membatasi T ke kelas dan kata kunci baru ().
Genotypek
28

Solusi paling sederhana Activator.CreateInstance<T>()

pengguna1471935
sumber
1
Terima kasih atas sarannya, itu membuat saya berada di tempat yang saya inginkan. Meskipun ini tidak memungkinkan Anda untuk menggunakan konstruktor berparameter. Tapi Anda bisa menggunakan varian non-generik: Activator.CreateInstance (typeof (T), objek baru [] {...}) di mana array objek berisi argumen untuk konstruktor.
Rob Vermeulen
19

Seperti yang ditunjukkan Jon, ini adalah masa pakai untuk membatasi konstruktor yang tidak memiliki parameter. Namun solusi yang berbeda adalah dengan menggunakan pola pabrik. Ini mudah dibatasi

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

Namun pilihan lain adalah menggunakan pendekatan fungsional. Lewati metode pabrik.

public void AddFruit<T>(Func<int,T> factoryDel) where T : BaseFruit { 
  BaseFruit fruit = factoryDel(weight); /* new Apple(150); */
  fruit.Enlist(fruitManager);
}
JaredPar
sumber
2
Saran yang bagus - walaupun jika Anda tidak hati-hati Anda bisa berakhir di neraka Java DOM API, dengan banyak pabrik :(
Jon Skeet
Ya, ini adalah solusi yang saya pertimbangkan sendiri. Tetapi saya berharap untuk sesuatu di garis kendala. Kira tidak ..
Boris Callens
@boris, sayangnya bahasa kendala yang Anda cari tidak ada pada saat ini
JaredPar
11

Anda dapat melakukannya dengan menggunakan refleksi:

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: Menambahkan konstruktor == cek kosong.

EDIT: Varian yang lebih cepat menggunakan cache:

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
sumber
Meskipun saya tidak suka overhead refleksi, seperti yang orang lain jelaskan, ini memang seperti sekarang ini. Melihat bagaimana konstruktor ini tidak dipanggil terlalu banyak, saya bisa menggunakan ini. Atau pabrik. Belum tahu
Boris Callens
Ini adalah pendekatan yang saya pilih saat ini karena tidak menambah kompleksitas di sisi doa
Rob Vermeulen
Tapi sekarang saya sudah membaca tentang saran Activator, yang memiliki nastiness serupa dengan solusi refleksi di atas, tetapi dengan lebih sedikit baris kode :) Saya akan pergi untuk opsi Activator.
Rob Vermeulen
1

Sebagai tambahan untuk saran pengguna1471935:

Untuk membuat instance kelas generik dengan menggunakan konstruktor dengan satu atau lebih parameter, Anda sekarang dapat menggunakan kelas Activator.

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

Daftar objek adalah parameter yang ingin Anda berikan. Menurut Microsoft :

CreateInstance [...] membuat turunan dari tipe yang ditentukan menggunakan konstruktor yang paling cocok dengan parameter yang ditentukan.

Ada juga versi generik dari CreateInstance ( CreateInstance<T>()) tetapi yang satu itu juga tidak memungkinkan Anda untuk menyediakan parameter konstruktor.

Rob Vermeulen
sumber
1

Saya membuat metode ini:

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

Saya menggunakannya dengan cara ini:

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

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

Kode:

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

B instanceB = new B();
instanceB = ConvertParentObjToChildObj<A,B>(instanceA);
Diocleziano Carletti
sumber
0

Baru-baru ini saya menemukan masalah yang sangat mirip. Hanya ingin berbagi solusi kami dengan Anda semua. Saya ingin saya membuat contoh dari Car<CarA>menggunakan objek json yang memiliki 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) });
farshid
sumber
-2

Itu masih mungkin, dengan kinerja tinggi, dengan melakukan hal berikut:

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

dan

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

Kelas-kelas yang relevan kemudian harus berasal dari antarmuka ini dan menginisialisasi sesuai. Harap dicatat, bahwa dalam kasus saya, kode ini adalah bagian dari kelas di sekitarnya, yang sudah memiliki <T> sebagai parameter generik. R, dalam kasus saya, juga adalah kelas read-only. IMO, ketersediaan publik dari fungsi Initialize () tidak memiliki efek negatif pada imutabilitas. Pengguna kelas ini bisa memasukkan objek lain, tetapi ini tidak akan mengubah koleksi yang mendasarinya.

cskwg
sumber