Melewati argumen ke C # generic new () dari tipe templated

409

Saya mencoba membuat objek tipe T baru melalui konstruktornya saat menambahkan ke daftar.

Saya mendapatkan kesalahan kompilasi: Pesan kesalahan adalah:

'T': tidak dapat memberikan argumen saat membuat turunan variabel

Tetapi kelas saya memang memiliki argumen konstruktor! Bagaimana saya bisa membuat ini berfungsi?

public static string GetAllItems<T>(...) where T : new()
{
   ...
   List<T> tabListItems = new List<T>();
   foreach (ListItem listItem in listCollection) 
   {
       tabListItems.Add(new T(listItem)); // error here.
   } 
   ...
}
LB
sumber
2
mungkin duplikat dari Buat instance dari tipe generik?
nawfal
2
Proposal untuk mendapatkan fungsi ini ke dalam bahasa: github.com/dotnet/roslyn/issues/2206
Ian Kemp
Dalam dokumentasi Microsoft, lihat Kesalahan Kompilasi CS0417 .
DavidRR
1
Proposal untuk mendapatkan fungsi ini ke dalam bahasa telah dipindahkan ke: github.com/dotnet/csharplang/issues/769
mengurangi aktivitas

Jawaban:

410

Untuk membuat turunan tipe generik dalam suatu fungsi, Anda harus membatasi dengan flag "baru".

public static string GetAllItems<T>(...) where T : new()

Namun itu hanya akan berfungsi ketika Anda ingin memanggil konstruktor yang tidak memiliki parameter. Tidak demikian di sini. Sebaliknya Anda harus memberikan parameter lain yang memungkinkan untuk membuat objek berdasarkan parameter. Yang paling mudah adalah fungsi.

public static string GetAllItems<T>(..., Func<ListItem,T> del) {
  ...
  List<T> tabListItems = new List<T>();
  foreach (ListItem listItem in listCollection) 
  {
    tabListItems.Add(del(listItem));
  }
  ...
}

Anda kemudian dapat menyebutnya seperti itu

GetAllItems<Foo>(..., l => new Foo(l));
JaredPar
sumber
Bagaimana ini bekerja ketika dipanggil secara internal dari kelas generik? Saya telah memposting kode saya dalam jawaban di bawah ini. Saya tidak tahu kelas konkret secara internal, karena ini adalah kelas generik. Apakah ada jalan memutar ini? Saya tidak ingin menggunakan saran lain dari menggunakan properti initialiser sintaks seperti itu akan memotong logika saya miliki di constructor
ChrisCa
menambahkan kode saya ke pertanyaan lain stackoverflow.com/questions/1682310/…
ChrisCa
21
Ini adalah salah satu batasan C # yang paling menyebalkan. Saya ingin membuat kelas saya tidak berubah: Memiliki pemukim pribadi yang hanya akan membuat kelas tidak mungkin berada dalam keadaan tidak valid dengan efek samping. Saya juga suka menggunakan fungsi dan lambda, tapi saya tahu itu masih menjadi masalah di dunia bisnis karena umumnya programmer belum tahu lambda dan ini membuat kelas Anda lebih sulit untuk dipahami.
Tuomas Hietanen
1
Terima kasih. Dalam kasus saya, saya tahu argumen konstruktor ketika saya memanggil metode, saya hanya perlu menyiasati batasan parameter Type yang tidak dapat dibangun dengan parameter, jadi saya menggunakan thunk . Ketukan adalah parameter opsional untuk metode ini, dan saya hanya menggunakannya jika diberikan: T result = thunk == null ? new T() : thunk(); Manfaat ini bagi saya adalah mengkonsolidasikan logika Tpenciptaan di satu tempat daripada terkadang membuat Tdi dalam dan kadang-kadang di luar metode.
Carl G
Saya pikir ini adalah salah satu tempat yang bahasa C # memutuskan untuk mengatakan tidak kepada programmer dan berhenti mengatakan ya sepanjang waktu! Meskipun pendekatan ini sedikit canggung cara membuat objek tetapi saya harus menggunakannya untuk saat ini.
AmirHossein Rezaei
331

di .Net 3.5 dan setelah Anda bisa menggunakan kelas aktivator:

(T)Activator.CreateInstance(typeof(T), args)
pengguna287107
sumber
1
kita juga bisa menggunakan pohon ekspresi untuk membangun objek
Welly Tambunan
4
Apa itu args? Sebuah Objek[]?
Rodney P. Barbati
3
Ya, args adalah objek [] di mana Anda menentukan nilai yang akan diberikan ke konstruktor T: "objek baru [] {par1, par2}"
TechNyquist
3
PERINGATAN: Jika Anda memiliki konstruktor khusus hanya demi Activator.CreateInstanceuntuk satu hal ini, itu akan terlihat seperti konstruktor Anda tidak digunakan sama sekali, dan seseorang mungkin mencoba "membersihkan" dan menghapusnya (untuk menyebabkan kesalahan runtime di beberapa waktu acak di masa depan). Anda mungkin ingin mempertimbangkan untuk menambahkan fungsi dummy tempat Anda menggunakan konstruktor ini sehingga Anda mendapatkan kesalahan kompilasi jika Anda mencoba menghapusnya.
jrh
51

Karena tidak ada yang mau memposting jawaban 'Refleksi' (yang menurut saya pribadi adalah jawaban terbaik), begini:

public static string GetAllItems<T>(...) where T : new()
{
   ...
   List<T> tabListItems = new List<T>();
   foreach (ListItem listItem in listCollection) 
   {
       Type classType = typeof(T);
       ConstructorInfo classConstructor = classType.GetConstructor(new Type[] { listItem.GetType() });
       T classInstance = (T)classConstructor.Invoke(new object[] { listItem });

       tabListItems.Add(classInstance);
   } 
   ...
}

Sunting: Jawaban ini sudah usang karena Activator.CreateInstance .NET 3.5, namun masih berguna dalam versi .NET yang lebih lama.

James Jones
sumber
Pemahaman saya adalah bahwa sebagian besar hit kinerja dalam memperoleh ConstructorInfo di tempat pertama. Jangan mengambil kata-kata saya untuk itu tanpa membuat profil. Jika itu masalahnya, cukup menyimpan ConstructorInfo untuk digunakan kembali nanti dapat mengurangi hit kinerja instantiations berulang melalui refleksi.
Kelsie
19
Saya pikir kurangnya pemeriksaan waktu kompilasi lebih memprihatinkan.
Dave Van den Eynde
1
@ James, saya setuju, saya terkejut tidak melihat ini sebagai "jawaban". Sebenarnya, saya mencari di pertanyaan ini berharap untuk menemukan contoh mudah yang bagus (seperti milik Anda) karena sudah begitu lama sejak saya melakukan refleksi. Bagaimanapun, +1 dari saya, tetapi +1 pada jawaban Activator juga. Saya melihat apa yang dilakukan Activator, dan ternyata apa yang dilakukan adalah refleksi yang direkayasa dengan sangat baik. :)
Mike
Panggilan GetConstructor () mahal, jadi itu layak untuk di-cache sebelum loop. Dengan cara ini, dengan hanya memanggil Invoke () di dalam loop, itu jauh lebih cepat daripada memanggil keduanya atau bahkan menggunakan Activator.CreateInstance ().
Cosmin Rus
30

Penginisialisasi objek

Jika konstruktor Anda dengan parameter tidak melakukan apa pun selain mengatur properti, Anda dapat melakukan ini di C # 3 atau lebih baik menggunakan penginisialisasi objek daripada memanggil konstruktor (yang tidak mungkin, seperti yang telah disebutkan):

public static string GetAllItems<T>(...) where T : new()
{
   ...
   List<T> tabListItems = new List<T>();
   foreach (ListItem listItem in listCollection) 
   {
       tabListItems.Add(new T() { YourPropertyName = listItem } ); // Now using object initializer
   } 
   ...
}

Dengan menggunakan ini, Anda selalu dapat memasukkan logika konstruktor di konstruktor default (kosong) juga.

Activator.CreateInstance ()

Atau, Anda dapat memanggil Activator.CreateInstance () seperti:

public static string GetAllItems<T>(...) where T : new()
{
   ...
   List<T> tabListItems = new List<T>();
   foreach (ListItem listItem in listCollection) 
   {
        object[] args = new object[] { listItem };
        tabListItems.Add((T)Activator.CreateInstance(typeof(T), args)); // Now using Activator.CreateInstance
   } 
   ...
}

Perhatikan bahwa Activator.CreateInstance dapat memiliki beberapa overhead kinerja yang mungkin ingin Anda hindari jika kecepatan eksekusi adalah prioritas utama dan opsi lain dapat dipertahankan untuk Anda.

Tim Lehner
sumber
ini mencegah Tdari melindungi invariannya (mengingat yang Tmemiliki> 0 dependensi atau nilai yang diperlukan, Anda sekarang dapat membuat instance Tyang dalam keadaan tidak valid / tidak dapat digunakan. kecuali Tada sesuatu yang mati sederhana seperti model tampilan DTO dan saya akan mengatakan hindari ini.
sara
20

Pertanyaan yang sangat lama, tetapi jawaban baru ;-)

Versi ExpressionTree : (Saya pikir solusi tercepat dan terbersih)

Seperti kata Welly Tambunan , "kita juga bisa menggunakan pohon ekspresi untuk membangun objek"

Ini akan menghasilkan 'konstruktor' (fungsi) untuk jenis / parameter yang diberikan. Ini mengembalikan delegasi dan menerima tipe parameter sebagai array objek.

Ini dia:

// this delegate is just, so you don't have to pass an object array. _(params)_
public delegate object ConstructorDelegate(params object[] args);

public static ConstructorDelegate CreateConstructor(Type type, params Type[] parameters)
{
    // Get the constructor info for these parameters
    var constructorInfo = type.GetConstructor(parameters);

    // define a object[] parameter
    var paramExpr = Expression.Parameter(typeof(Object[]));

    // To feed the constructor with the right parameters, we need to generate an array 
    // of parameters that will be read from the initialize object array argument.
    var constructorParameters = parameters.Select((paramType, index) =>
        // convert the object[index] to the right constructor parameter type.
        Expression.Convert(
            // read a value from the object[index]
            Expression.ArrayAccess(
                paramExpr,
                Expression.Constant(index)),
            paramType)).ToArray();

    // just call the constructor.
    var body = Expression.New(constructorInfo, constructorParameters);

    var constructor = Expression.Lambda<ConstructorDelegate>(body, paramExpr);
    return constructor.Compile();
}

Contoh MyClass:

public class MyClass
{
    public int TestInt { get; private set; }
    public string TestString { get; private set; }

    public MyClass(int testInt, string testString)
    {
        TestInt = testInt;
        TestString = testString;
    }
}

Pemakaian:

// you should cache this 'constructor'
var myConstructor = CreateConstructor(typeof(MyClass), typeof(int), typeof(string));

// Call the `myConstructor` function to create a new instance.
var myObject = myConstructor(10, "test message");

masukkan deskripsi gambar di sini


Contoh lain: melewatkan tipe sebagai array

var type = typeof(MyClass);
var args = new Type[] { typeof(int), typeof(string) };

// you should cache this 'constructor'
var myConstructor = CreateConstructor(type, args);

// Call the `myConstructor` fucntion to create a new instance.
var myObject = myConstructor(10, "test message");

DebugView dari Ekspresi

.Lambda #Lambda1<TestExpressionConstructor.MainWindow+ConstructorDelegate>(System.Object[] $var1) {
    .New TestExpressionConstructor.MainWindow+MyClass(
        (System.Int32)$var1[0],
        (System.String)$var1[1])
}

Ini sama dengan kode yang dihasilkan:

public object myConstructor(object[] var1)
{
    return new MyClass(
        (System.Int32)var1[0],
        (System.String)var1[1]);
}

Kelemahan kecil

Semua parameter valuetipe kotak ketika mereka dilewatkan seperti array objek.


Tes kinerja sederhana:

private void TestActivator()
{
    Stopwatch sw = Stopwatch.StartNew();
    for (int i = 0; i < 1024 * 1024 * 10; i++)
    {
        var myObject = Activator.CreateInstance(typeof(MyClass), 10, "test message");
    }
    sw.Stop();
    Trace.WriteLine("Activator: " + sw.Elapsed);
}

private void TestReflection()
{
    var constructorInfo = typeof(MyClass).GetConstructor(new[] { typeof(int), typeof(string) });

    Stopwatch sw = Stopwatch.StartNew();
    for (int i = 0; i < 1024 * 1024 * 10; i++)
    {
        var myObject = constructorInfo.Invoke(new object[] { 10, "test message" });
    }

    sw.Stop();
    Trace.WriteLine("Reflection: " + sw.Elapsed);
}

private void TestExpression()
{
    var myConstructor = CreateConstructor(typeof(MyClass), typeof(int), typeof(string));

    Stopwatch sw = Stopwatch.StartNew();

    for (int i = 0; i < 1024 * 1024 * 10; i++)
    {
        var myObject = myConstructor(10, "test message");
    }

    sw.Stop();
    Trace.WriteLine("Expression: " + sw.Elapsed);
}

TestActivator();
TestReflection();
TestExpression();

Hasil:

Activator: 00:00:13.8210732
Reflection: 00:00:05.2986945
Expression: 00:00:00.6681696

Menggunakan Expressions+/- 8 kali lebih cepat daripada Memanggil ConstructorInfodan +/- 20 kali lebih cepat daripada menggunakanActivator

Jeroen van Langen
sumber
Apakah Anda memiliki wawasan tentang apa yang harus dilakukan jika Anda ingin membuat MyClass <T> dengan publik konstruktor MyClass (data T). Dalam hal ini, Expression.Convert melempar pengecualian dan jika saya menggunakan kelas dasar kendala generik untuk mengkonversi, maka Expression. Baru melempar karena info konstruktor adalah untuk tipe generik
Mason
@ Alasan (butuh beberapa saat untuk menjawab ;-)) var myConstructor = CreateConstructor(typeof(MyClass<int>), typeof(int));ini berfungsi dengan baik. Saya tidak tahu
Jeroen van Langen
19

Ini tidak akan berfungsi dalam situasi Anda. Anda hanya dapat menentukan batasan yang memiliki konstruktor kosong:

public static string GetAllItems<T>(...) where T: new()

Apa yang bisa Anda lakukan adalah menggunakan injeksi properti dengan mendefinisikan antarmuka ini:

public interface ITakesAListItem
{
   ListItem Item { set; }
}

Maka Anda dapat mengubah metode Anda menjadi ini:

public static string GetAllItems<T>(...) where T : ITakesAListItem, new()
{
   ...
   List<T> tabListItems = new List<T>();
   foreach (ListItem listItem in listCollection) 
   {
       tabListItems.Add(new T() { Item = listItem });
   } 
   ...
}

Alternatif lain adalah Funcmetode yang dijelaskan oleh JaredPar.

Garry Shutler
sumber
ini akan mem-bypass logika apa pun yang ada dalam konstruktor yang mengambil argumen, kan? Saya ingin melakukan pendekatan sesuatu Seperti Jared tapi saya memanggil metode internal di dalam kelas sehingga tidak tahu apa jenis beton adalah ... hmmm
ChrisCa
3
Benar, ini menyebut logika konstruktor default T (), lalu cukup setel properti "Item". Jika Anda mencoba mengaktifkan logika konstruktor non-default, ini tidak akan membantu Anda.
Scott Stafford
7

Anda perlu menambahkan di mana T: new () agar kompiler tahu bahwa T dijamin untuk menyediakan konstruktor default.

public static string GetAllItems<T>(...) where T: new()
Richard
sumber
1
UPDATE: Pesan galat yang benar adalah: 'T': tidak dapat memberikan argumen saat membuat turunan variabel
LB.
Itu karena Anda tidak menggunakan konstruktor kosong, Anda memberikan argumen padanya tentang objek. Tidak ada cara untuk mengatasinya tanpa menentukan bahwa Tipe generik memiliki parameter (objek) baru.
Min
Maka Anda akan perlu: 1. Gunakan refleksi 2. Lewatkan parameter ke dalam metode inisialisasi daripada konstruktor, di mana metode inisialisasi milik antarmuka yang mengimplementasikan jenis Anda dan yang termasuk dalam di mana T: ... pernyataan. Opsi 1 adalah dampak terendah untuk sisa kode Anda, tetapi opsi 2 memberikan waktu kompilasi pengecekan.
Richard
Jangan gunakan refleksi! Ada beberapa cara lain yang diuraikan dalam jawaban lain yang memberi Anda efek yang sama.
Garry Shutler
@ Garry - Saya setuju bahwa refleksi belum tentu merupakan pendekatan terbaik, tetapi memungkinkan Anda untuk mencapai apa yang diperlukan dengan sedikit perubahan ke seluruh basis kode. Yang mengatakan, saya lebih suka pendekatan delegasi pabrik dari @JaredPar.
Richard
7

Jika Anda hanya ingin menginisialisasi bidang anggota atau properti dengan parameter konstruktor, dalam C #> = 3 Anda dapat melakukannya dengan lebih mudah:

public static string GetAllItems<T>(...) where T : InterfaceOrBaseClass, new() 
{ 
   ... 
   List<T> tabListItems = new List<T>(); 
   foreach (ListItem listItem in listCollection)  
   { 
       tabListItems.Add(new T{ BaseMemberItem = listItem }); // No error, BaseMemberItem owns to InterfaceOrBaseClass. 
   }  
   ... 
} 

Ini adalah hal yang sama yang dikatakan Garry Shutler, tetapi saya ingin memberikan catatan tambahan.

Tentu saja Anda dapat menggunakan trik properti untuk melakukan lebih banyak hal daripada sekadar menetapkan nilai bidang. Properti "set ()" dapat memicu pemrosesan yang diperlukan untuk mensetup bidang terkait dan kebutuhan lain untuk objek itu sendiri, termasuk pemeriksaan untuk melihat apakah inisialisasi penuh akan terjadi sebelum objek digunakan, mensimulasikan konstruksi penuh ( ya, ini adalah solusi buruk, tetapi mengatasi batasan M $ baru ()).

Saya tidak dapat memastikan apakah itu lubang yang direncanakan atau efek samping yang tidak disengaja, tetapi berhasil.

Sangat lucu bagaimana orang MS menambahkan fitur baru ke bahasa dan tampaknya tidak melakukan analisis efek samping penuh. Seluruh hal generik adalah bukti yang baik dari ini ...

fljx
sumber
1
Kedua kendala itu dibutuhkan. InterfaceOrBaseClass membuat kompiler mengetahui bidang / properti BaseMemberItem. Jika batasan "new ()" dikomentari, itu akan memicu kesalahan: Kesalahan 6 Tidak dapat membuat turunan dari tipe variabel 'T' karena tidak memiliki batasan baru ()
fljx
Situasi yang saya temui tidak persis seperti pertanyaan yang diajukan di sini, namun jawaban ini membuat saya ke mana saya harus pergi dan tampaknya bekerja dengan sangat baik.
RubyHaus
5
Setiap kali seseorang menyebut Microsoft sebagai "M $", sebagian kecil dari jiwaku menderita.
Mathias Lykkegaard Lorenzen
6

Saya menemukan bahwa saya mendapatkan kesalahan "tidak dapat memberikan argumen saat membuat turunan dari tipe parameter T" jadi saya perlu melakukan ini:

var x = Activator.CreateInstance(typeof(T), args) as T;
chris31389
sumber
5

Jika Anda memiliki akses ke kelas yang akan Anda gunakan, Anda dapat menggunakan pendekatan ini yang saya gunakan.

Buat antarmuka yang memiliki pencipta alternatif:

public interface ICreatable1Param
{
    void PopulateInstance(object Param);
}

Buat kelas Anda dengan pembuat kosong dan terapkan metode ini:

public class MyClass : ICreatable1Param
{
    public MyClass() { //do something or nothing }
    public void PopulateInstance (object Param)
    {
        //populate the class here
    }
}

Sekarang gunakan metode generik Anda:

public void MyMethod<T>(...) where T : ICreatable1Param, new()
{
    //do stuff
    T newT = new T();
    T.PopulateInstance(Param);
}

Jika Anda tidak memiliki akses, bungkus kelas target:

public class MyClass : ICreatable1Param
{
    public WrappedClass WrappedInstance {get; private set; }
    public MyClass() { //do something or nothing }
    public void PopulateInstance (object Param)
    {
        WrappedInstance = new WrappedClass(Param);
    }
}
Daniel Möller
sumber
0

Ini adalah jenis mucky, dan ketika saya mengatakan jenis mucky mungkin maksud saya memberontak, tetapi seandainya Anda dapat melengkapi tipe parameterised Anda dengan konstruktor kosong, maka:

public static T GetTInstance<T>() where T: new()
{
    var constructorTypeSignature = new Type[] {typeof (object)};
    var constructorParameters = new object[] {"Create a T"};
    return (T) new T().GetType().GetConstructor(constructorTypeSignature).Invoke(constructorParameters);
}

Secara efektif akan memungkinkan Anda untuk membangun objek dari tipe parameter dengan argumen. Dalam hal ini saya mengasumsikan konstruktor yang saya inginkan memiliki argumen tipe tunggal object. Kami membuat contoh tiruan dari T menggunakan konstruktor kosong yang diizinkan dan kemudian menggunakan refleksi untuk mendapatkan salah satu konstruktor lainnya.

silasdavis
sumber
0

Saya terkadang menggunakan pendekatan yang mirip dengan jawaban menggunakan injeksi properti, tetapi menjaga kode tetap bersih. Alih-alih memiliki kelas dasar / antarmuka dengan sekumpulan properti, ia hanya berisi inisialisasi (virtual) () - metode yang bertindak sebagai "konstruktor orang miskin". Kemudian Anda dapat membiarkan setiap kelas menangani inisialisasi sendiri seperti yang dilakukan oleh konstruktor, yang juga menambahkan cara yang nyaman untuk menangani rantai pewarisan.

Jika sering menemukan diri saya dalam situasi di mana saya ingin setiap kelas dalam rantai untuk menginisialisasi properti uniknya, dan kemudian memanggil metode Initialize () - induknya yang pada gilirannya menginisialisasi properti unik orangtua dan sebagainya. Ini sangat berguna ketika memiliki kelas yang berbeda, tetapi dengan hierarki yang sama, misalnya objek bisnis yang dipetakan ke / dari DTO: s.

Contoh yang menggunakan Kamus umum untuk inisialisasi:

void Main()
{
    var values = new Dictionary<string, int> { { "BaseValue", 1 }, { "DerivedValue", 2 } };

    Console.WriteLine(CreateObject<Base>(values).ToString());

    Console.WriteLine(CreateObject<Derived>(values).ToString());
}

public T CreateObject<T>(IDictionary<string, int> values)
    where T : Base, new()
{
    var obj = new T();
    obj.Initialize(values);
    return obj;
}

public class Base
{
    public int BaseValue { get; set; }

    public virtual void Initialize(IDictionary<string, int> values)
    {
        BaseValue = values["BaseValue"];
    }

    public override string ToString()
    {
        return "BaseValue = " + BaseValue;
    }
}

public class Derived : Base
{
    public int DerivedValue { get; set; }

    public override void Initialize(IDictionary<string, int> values)
    {
        base.Initialize(values);
        DerivedValue = values["DerivedValue"];
    }

    public override string ToString()
    {       
        return base.ToString() + ", DerivedValue = " + DerivedValue;
    }
}
Anders
sumber
0

Jika semua yang Anda butuhkan adalah konversi dari ListItem ke tipe T Anda, Anda dapat mengimplementasikan konversi ini di kelas T sebagai operator konversi.

public class T
{
    public static implicit operator T(ListItem listItem) => /* ... */;
}

public static string GetAllItems(...)
{
    ...
    List<T> tabListItems = new List<T>();
    foreach (ListItem listItem in listCollection) 
    {
        tabListItems.Add(listItem);
    } 
    ...
}
Mimpi burukZ
sumber
-4

Saya percaya Anda harus membatasi T dengan pernyataan di mana hanya mengizinkan objek dengan konstruktor baru.

HANYA sekarang ia menerima apa saja termasuk benda tanpa itu.

klkitchens
sumber
1
Anda mungkin ingin mengubah jawaban ini karena ini diedit ke pertanyaan setelah Anda menjawab yang membuat jawaban ini keluar dari konteks.
antar