2016-07-19 202 views
9

Preferisco utilizzare IEnumerable<object>, per i metodi di estensione LINQ sono definiti su di esso, non IEnumerable, in modo che possa utilizzare, ad esempio, range.Skip(2). Tuttavia, preferisco anche usare IEnumerable, per T[] è implicitamente convertibile in IEnumerable se T è un tipo di riferimento o un tipo di valore. Per quest'ultimo caso, non è coinvolta la boxe, il che è positivo. Di conseguenza, posso fare IEnumerable range = new[] { 1, 2, 3 }. Sembra impossibile combinare il meglio di entrambi i mondi. Comunque, ho scelto di sistemare a IEnumerable e fare una specie di cast quando ho bisogno di applicare i metodi LINQ.Trasmetti da IEnumerable a IEnumerable <object>

Da this SO thread, vengo a sapere che range.Cast<object>() è in grado di fare il lavoro. Ma incorre in spese generali di prestazioni che non è necessario a mio parere. Ho provato a eseguire un cast diretto in fase di compilazione come (IEnumerable<object>)range. Secondo i miei test, funziona per il tipo di elemento di riferimento ma non per il tipo di valore. Qualche idea?

FYI, la domanda deriva dal problema GitHub this. E il codice di prova che ho usato è il seguente:

static void Main(string[] args) 
{ 
    // IEnumerable range = new[] { 1, 2, 3 }; // won't work 
    IEnumerable range = new[] { "a", "b", "c" }; 
    var range2 = (IEnumerable<object>)range; 
    foreach (var item in range2) 
    { 
     Console.WriteLine(item); 
    } 
} 
+0

Vuoi davvero dire '' IEnumerable , o intendi 'IEnumerable '? Se il primo, perché usi '' e non ''? – Maarten

+0

@Maarten Sì, intendo usare 'IEnumerable ', perché devo gestire il caso generale e supportare diversi tipi di elementi. – Lingxi

+3

Perché dovresti lavorare con 'IEnumerable' in primo luogo? Perché no 'IEnumerable range = new [] {1, 2, 3};' invece? - Btw. 'IEnumerable' * fa * box, giusto? Se si 'foreach' su un (non generico)' IEnumerable', si ottiene un 'oggetto'. A * generic * enumerable ('IEnumerable ') d'altra parte fa * not * box. – Corak

risposta

0

Dopo decompilazione che l'estensione, la fonte ha mostrato questo:

public static IEnumerable<TResult> Cast<TResult>(this IEnumerable source) 
    { 
     IEnumerable<TResult> enumerable = source as IEnumerable<TResult>; 
     if (enumerable != null) 
     return enumerable; 
     if (source == null) 
     throw Error.ArgumentNull("source"); 
     return Enumerable.CastIterator<TResult>(source); 
    } 

    private static IEnumerable<TResult> CastIterator<TResult>(IEnumerable source) 
    { 
     foreach (TResult result in source) 
     yield return result; 
    } 

Questo fondamentalmente fa altro che IEnumerable<object> al primo posto.

Lei ha affermato:

Secondo le mie prove, funziona per il tipo di elemento di riferimento, ma non per tipo di valore.

Come hai provato?

+0

Ho mostrato il codice di test nella domanda aggiornata. – Lingxi

1

IEnumerable<T> è un'interfaccia generica. Finché hai a che fare solo con generici e tipi noti in fase di compilazione, non ha senso usare IEnumerable<object> - utilizzare IEnumerable<int> o IEnumerable<T>, a seconda che tu stia scrivendo un metodo generico o uno in cui il tipo corretto sia già noto. Non cercare di trovare un oggetto IEnumerable adatto a tutti - usa quello corretto in primo luogo - è molto raro che ciò non sia possibile, e il più delle volte, è semplicemente il risultato di un cattivo design dell'oggetto.

La ragione per cui IEnumerable<int> non può essere trasmessa a IEnumerable<object> può essere un po 'sorprendente, ma in realtà è molto semplice - i tipi di valore non sono polimorfici, quindi non supportano la co-varianza. Non essere scambiato - IEnumerable<string> non implementareIEnumerable<object> - l'unica ragione per cui è possibile lanciare IEnumerable<string> a IEnumerable<object> è che IEnumerable<T> è co-variante.

È solo un caso divertente di "sorprendente, ma ovvio". È sorprendente, dal momento che int deriva da object, giusto? E tuttavia, è ovvio, perché non lo fa intdavvero derivano da object, anche se può essere gettati a un object attraverso un processo chiamato di pugilato, che crea un "vero e proprio oggetto int-derivato".

+0

Devo usare 'IEnumerable ', perché ho bisogno di supportare diversi tipi di elementi (es. Polimorfismo di runtime). – Lingxi

+0

@Lingxi Se questo è il caso, utilizzare 'IEnumerable ' come ultima risorsa e ottenerlo eseguendo '.Cast '. Per i tipi di riferimento, farà un cast semplice (dato che sono co-variant), per i tipi di valore, farà la stessa cosa 'IEnumerable'. – Luaan

5

Secondo i miei test, funziona per il tipo di elemento di riferimento ma non per il tipo di valore .

Corretto. Questo perché IEnumerable<out T> è in co-variante e co-variance/contra-variance is not supported for value types.

Vengo a sapere che range.Cast() è in grado di fare il lavoro. Ma è lo a sostenere un overhead delle prestazioni che a mio avviso non è necessario.

IMO il costo delle prestazioni (offerto dal pugilato) è inevitabile se si desidera una raccolta di oggetti con una raccolta di tipi di valori dati. L'utilizzo del codice non generico IEnumerable non eviterà la boxe perché fornisce un IEnumerator la cui proprietà .Current restituisce un object. Preferirei sempre usare IEnumerable<T> anziché IEnumerable. Quindi basta usare il metodo .Cast e dimenticare la boxe.

1

Nonostante io davvero non mi piace questo approccio, lo so che è possibile fornire un set di strumenti simili a LINQ to Objects che è richiamabile direttamente su un'interfaccia IEnumerable, senza forzare un cast di IEnumerable<object> (cattivo: possibile boxe!) e senza casting a IEnumerable<TFoo> (anche peggio: avremmo bisogno di sapere e scrivere TFoo!).

Tuttavia, è:

  • non libero per il tempo di esecuzione: si può essere pesante, non ho eseguito test di performance
  • non libero per gli sviluppatori: hai veramente bisogno di scrivere tutti coloro LINQ -come i metodi di estensione per IEnumerable (o trovare una lib che lo faccia)
  • non semplice: è necessario ispezionare attentamente il tipo in entrata e fare attenzione con molte possibili opzioni
  • non è un oracolo: data una raccolta che strumento s IEnumerable ma non implementa IEnumerable<T> solo può gettare errore o silenziosamente gettarlo ai IEnumerable<object>
  • non sempre funziona: data una collezione che implementa sia IEnumerable<int> e IEnumerable<string> semplicemente non può sapere che cosa fare; anche rinunciare e colata a IEnumerable<object> non suona proprio qui

Ecco un esempio per .Net4 +:

using System; 
using System.Linq; 
using System.Collections.Generic; 

class Program 
{ 
    public static void Main() 
    { 
     Console.WriteLine("List<int>"); 
     new List<int> { 1, 2, 3 } 
      .DoSomething() 
      .DoSomething(); 

     Console.WriteLine("List<string>"); 
     new List<string> { "a", "b", "c" } 
      .DoSomething() 
      .DoSomething(); 

     Console.WriteLine("int[]"); 
     new int[] { 1, 2, 3 } 
      .DoSomething() 
      .DoSomething(); 

     Console.WriteLine("string[]"); 
     new string[] { "a", "b", "c" } 
      .DoSomething() 
      .DoSomething(); 

     Console.WriteLine("nongeneric collection with ints"); 
     var stack = new System.Collections.Stack(); 
     stack.Push(1); 
     stack.Push(2); 
     stack.Push(3); 
     stack 
      .DoSomething() 
      .DoSomething(); 

     Console.WriteLine("nongeneric collection with mixed items"); 
     new System.Collections.ArrayList { 1, "a", null } 
      .DoSomething() 
      .DoSomething(); 

     Console.WriteLine("nongeneric collection with .. bits"); 
     new System.Collections.BitArray(0x6D) 
      .DoSomething() 
      .DoSomething(); 
    } 
} 

public static class MyGenericUtils 
{ 
    public static System.Collections.IEnumerable DoSomething(this System.Collections.IEnumerable items) 
    { 
     // check the REAL type of incoming collection 
     // if it implements IEnumerable<T>, we're lucky! 
     // we can unwrap it 
     // ...usually. How to unwrap it if it implements it multiple times?! 
     var ietype = items.GetType().FindInterfaces((t, args) => 
      t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>), 
      null).SingleOrDefault(); 

     if (ietype != null) 
     { 
      return 
       doSomething_X(
        doSomething_X((dynamic)items) 
       ); 
       // .doSomething_X() - and since the compile-time type is 'dynamic' I cannot chain 
       // .doSomething_X() - it in normal way (despite the fact it would actually compile well) 
       // `dynamic` doesn't resolve extension methods! 
       // it would look for doSomething_X inside the returned object 
       // ..but that's just INTERNAL implementation. For the user 
       // on the outside it's chainable 
     } 
     else 
      // uh-oh. no what? it can be array, it can be a non-generic collection 
      // like System.Collections.Hashtable .. but.. 
      // from the type-definition point of view it means it holds any 
      // OBJECTs inside, even mixed types, and it exposes them as IEnumerable 
      // which returns them as OBJECTs, so.. 
      return items.Cast<object>() 
       .doSomething_X() 
       .doSomething_X(); 
    } 

    private static IEnumerable<T> doSomething_X<T>(this IEnumerable<T> valitems) 
    { 
     // do-whatever,let's just see it being called 
     Console.WriteLine("I got <{1}>: {0}", valitems.Count(), typeof(T)); 
     return valitems; 
    } 
} 

Sì, è stupido. Li ho incatenati quattro volte (2outsidex2inside) solo per mostrare che le informazioni sul tipo sono non perse nelle chiamate successive. Il punto era dimostrare che il "punto di ingresso" prendesse un valore non generico IEnumerable e che <T> sia risolto ovunque possa essere. È possibile adattare facilmente il codice per renderlo un normale metodo LINQ-to-Objects .Count(). Allo stesso modo, si possono scrivere anche tutte le altre operazioni.

Questo esempio utilizza dynamic per consentire alla piattaforma di risolvere la T più stretta per IEnumerable, se possibile (che è necessario garantire per prima). Senza dynamic (ovvero .Net2.0) avremmo bisogno di invocare il dosomething_X tramite riflessione, o implementarlo due volte come dosomething_refs<T>():where T:class + dosomething_vals<T>():where T:struct e fare un po 'di magia per chiamarlo correttamente senza effettivamente il casting (probabilmente il riflesso, di nuovo).

Tuttavia, sembra che si possa ottenere qualcosa-come-linq lavorando "direttamente" su cose nascoste dietro IEnumerable non generico. Tutto grazie al fatto che gli oggetti che si nascondono dietro a IEnumerable hanno ancora le proprie informazioni complete sul tipo (sì, questa ipotesi potrebbe non funzionare con COM o Remoting). Tuttavia .. Penso che accontentarsi di IEnumerable<T> è un'opzione migliore. Lasciamo semplicemente il vecchio IEnumerable in casi speciali in cui non c'è davvero altra opzione.

..oh .. e io in realtà non ha verificato se il codice di cui sopra è corretto, veloce, sicuro, risparmio delle risorse, pigro-valutazione, ecc

+0

Wow ~ Sono sorpreso di sicuro. (@ _ @) – Lingxi