In che modo gli array in C # implementano parzialmente IList <T>?


99

Quindi, come forse saprai, gli array in C # implementano IList<T>, tra le altre interfacce. In qualche modo, però, lo fanno senza implementare pubblicamente la proprietà Count di IList<T>! Gli array hanno solo una proprietà Length.

È un esempio lampante di C # /. NET che infrange le proprie regole sull'implementazione dell'interfaccia o mi manca qualcosa?


2
Nessuno ha detto che la Arrayclasse doveva essere scritta in C #!
user541686

Arrayè una classe "magica", che non può essere implementata in C # o in qualsiasi altro linguaggio con targeting .net. Ma questa funzionalità specifica è disponibile in C #.
CodesInChaos

Risposte:


81

Nuova risposta alla luce della risposta di Hans

Grazie alla risposta data da Hans, possiamo vedere che l'implementazione è un po 'più complicata di quanto potremmo pensare. Sia il compilatore e il CLR prova molto duro per dare l'impressione che un tipo di matrice attrezzi IList<T>- ma varianza gamma rende questo più complicato. Contrariamente alla risposta di Hans, i tipi di matrice (unidimensionale, in ogni caso a base zero) implementano direttamente le raccolte generiche, perché il tipo di un array specifico non lo è System.Array , è solo il tipo di base dell'array. Se chiedi a un tipo di array quali interfacce supporta, include i tipi generici:

foreach (var type in typeof(int[]).GetInterfaces())
{
    Console.WriteLine(type);
}

Produzione:

System.ICloneable
System.Collections.IList
System.Collections.ICollection
System.Collections.IEnumerable
System.Collections.IStructuralComparable
System.Collections.IStructuralEquatable
System.Collections.Generic.IList`1[System.Int32]
System.Collections.Generic.ICollection`1[System.Int32]
System.Collections.Generic.IEnumerable`1[System.Int32]

Per array monodimensionali a base zero, per quanto riguarda il linguaggio , anche l'array implementa IList<T>davvero. La sezione 12.1.2 della specifica C # lo dice. Quindi, qualunque sia l'implementazione sottostante, il linguaggio deve comportarsi come se il tipo di T[]implementa IList<T>come con qualsiasi altra interfaccia. Da questo punto di vista, l'interfaccia è implementata con alcuni dei membri che vengono implementati esplicitamente (come Count). Questa è la migliore spiegazione a livello di lingua per quello che sta succedendo.

Si noti che questo vale solo per gli array unidimensionali (e gli array a base zero, non che il linguaggio C # dica qualcosa sugli array non a base zero). T[,] non implementa IList<T>.

Dal punto di vista di CLR, sta succedendo qualcosa di più funky. Non è possibile ottenere la mappatura dell'interfaccia per i tipi di interfaccia generici. Per esempio:

typeof(int[]).GetInterfaceMap(typeof(ICollection<int>))

Dà un'eccezione di:

Unhandled Exception: System.ArgumentException: Interface maps for generic
interfaces on arrays cannot be retrived.

Allora perché la stranezza? Bene, credo che sia davvero dovuto alla covarianza degli array, che è una verruca nel sistema dei tipi, IMO. Anche se nonIList<T> è covariante (e non può essere sicuro), la covarianza dell'array consente a questo di funzionare:

string[] strings = { "a", "b", "c" };
IList<object> objects = strings;

... il che lo fa sembrare un typeof(string[])attrezzo IList<object>, quando in realtà non lo è.

La partizione 1 delle specifiche CLI (ECMA-335), sezione 8.7.1, ha questo:

Un tipo di firma T è compatibile con un tipo di firma U se e solo se almeno una delle seguenti condizioni è valida

...

T è un array di rango 1 a base zero V[], ed Uè IList<W>, e V è compatibile con l'elemento dell'array W.

(In realtà non menziona ICollection<W>o IEnumerable<W>che credo sia un bug nelle specifiche.)

Per non varianza, la specifica CLI va di pari passo con la specifica della lingua. Dalla sezione 8.9.1 della partizione 1:

Inoltre, un vettore creato con il tipo di elemento T, implementa l'interfaccia System.Collections.Generic.IList<U>, dove U: = T. (§8.7)

(Un vettore è un array unidimensionale con una base zero.)

Ora, in termini di dettagli di implementazione , chiaramente il CLR sta facendo un po 'di mappatura funky per mantenere la compatibilità dell'assegnazione qui: quando a string[]viene richiesta l'implementazione di ICollection<object>.Count, non può gestirlo in modo abbastanza normale. Questo conta come implementazione esplicita dell'interfaccia? Penso che sia ragionevole trattarlo in questo modo, poiché a meno che tu non chieda direttamente la mappatura dell'interfaccia, si comporta sempre in quel modo dal punto di vista del linguaggio.

Di cosa ICollection.Count?

Finora ho parlato delle interfacce generiche, ma poi c'è il non generico ICollectioncon la sua Countproprietà. Questa volta siamo in grado di ottenere la mappatura di interfaccia, e in effetti l'interfaccia è implementata direttamente System.Array. La documentazione per l' ICollection.Countimplementazione della proprietà Arrayafferma che è implementata con l'implementazione dell'interfaccia esplicita.

Se qualcuno può pensare a un modo in cui questo tipo di implementazione esplicita dell'interfaccia è diverso dalla "normale" implementazione esplicita dell'interfaccia, sarei felice di esaminarlo ulteriormente.

Vecchia risposta sull'implementazione dell'interfaccia esplicita

Nonostante quanto sopra, che è più complicato a causa della conoscenza degli array, puoi ancora fare qualcosa con gli stessi effetti visibili attraverso l' implementazione dell'interfaccia esplicita .

Ecco un semplice esempio autonomo:

public interface IFoo
{
    void M1();
    void M2();
}

public class Foo : IFoo
{
    // Explicit interface implementation
    void IFoo.M1() {}

    // Implicit interface implementation
    public void M2() {}
}

class Test    
{
    static void Main()
    {
        Foo foo = new Foo();

        foo.M1(); // Compile-time failure
        foo.M2(); // Fine

        IFoo ifoo = foo;
        ifoo.M1(); // Fine
        ifoo.M2(); // Fine
    }
}

5
Penso che avrai un errore in fase di compilazione su foo.M1 (); non foo.M2 ();
Kevin Aenmey

La sfida qui è avere una classe non generica, come un array, che implementa un tipo di interfaccia generico, come IList <>. Il tuo snippet non lo fa.
Hans Passant

@HansPassant: È molto facile fare in modo che una classe non generica implementi un tipo di interfaccia generico. Banale. Non vedo alcuna indicazione che sia quello che chiedeva l'OP.
Jon Skeet

4
@ JohnSaunders: In realtà, non credo che prima fosse impreciso. L'ho ampliato molto e ho spiegato perché CLR tratta gli array in modo strano, ma credo che la mia risposta sull'implementazione dell'interfaccia esplicita fosse abbastanza corretta prima. In che modo non sei d'accordo? Ancora una volta, i dettagli sarebbero utili (possibilmente nella tua risposta, se appropriato).
Jon Skeet

1
@RBT: Sì, anche se c'è una differenza in quanto l'uso Countva bene, ma Addverrà sempre generato, poiché gli array sono di dimensioni fisse.
Jon Skeet

86

Quindi, come forse saprai, gli array in C # implementano IList<T>, tra le altre interfacce

Ebbene, sì, ehm no, non proprio. Questa è la dichiarazione per la classe Array nel framework .NET 4:

[Serializable, ComVisible(true)]
public abstract class Array : ICloneable, IList, ICollection, IEnumerable, 
                              IStructuralComparable, IStructuralEquatable
{
    // etc..
}

Implementa System.Collections.IList, non System.Collections.Generic.IList <>. Non può, Array non è generico. Lo stesso vale per le interfacce generiche IEnumerable <> e ICollection <>.

Ma il CLR crea tipi di array concreti al volo, quindi potrebbe tecnicamente crearne uno che implementa queste interfacce. Tuttavia non è così. Prova questo codice ad esempio:

using System;
using System.Collections.Generic;

class Program {
    static void Main(string[] args) {
        var goodmap = typeof(Derived).GetInterfaceMap(typeof(IEnumerable<int>));
        var badmap = typeof(int[]).GetInterfaceMap(typeof(IEnumerable<int>));  // Kaboom
    }
}
abstract class Base { }
class Derived : Base, IEnumerable<int> {
    public IEnumerator<int> GetEnumerator() { return null; }
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); }
}

La chiamata GetInterfaceMap () non riesce per un tipo di matrice concreta con "Interfaccia non trovata". Eppure un cast a IEnumerable <> funziona senza problemi.

Questa è la digitazione da ciarlatani. È lo stesso tipo di digitazione che crea l'illusione che ogni tipo di valore derivi da ValueType che deriva da Object. Sia il compilatore che il CLR hanno una conoscenza speciale dei tipi di array, proprio come fanno dei tipi di valore. Il compilatore vede il tuo tentativo di trasmettere a IList <> e dice "okay, so come farlo!". Ed emette l'istruzione IL castclass. Il CLR non ha problemi, sa come fornire un'implementazione di IList <> che funziona sull'oggetto array sottostante. Ha una conoscenza incorporata della classe System.SZArrayHelper altrimenti nascosta, un wrapper che implementa effettivamente queste interfacce.

Cosa che non fa esplicitamente come affermano tutti, la proprietà Count di cui hai chiesto è simile a questa:

    internal int get_Count<T>() {
        //! Warning: "this" is an array, not an SZArrayHelper. See comments above
        //! or you may introduce a security hole!
        T[] _this = JitHelpers.UnsafeCast<T[]>(this);
        return _this.Length;
    }

Sì, puoi certamente chiamare quel commento "infrangere le regole" :) Altrimenti è dannatamente utile. Ed estremamente ben nascosto, puoi verificarlo in SSCLI20, la distribuzione dei sorgenti condivisi per CLR. Cerca "IList" per vedere dove avviene la sostituzione del tipo. Il posto migliore per vederlo in azione è clr / src / vm / array.cpp, metodo GetActualImplementationForArrayGenericIListMethod ().

Questo tipo di sostituzione nel CLR è piuttosto blando rispetto a quanto accade nella proiezione del linguaggio nel CLR che consente la scrittura di codice gestito per WinRT (aka Metro). Quasi tutti i tipi .NET di base vengono sostituiti lì. IList <> esegue il mapping a IVector <>, ad esempio, un tipo completamente non gestito. Di per sé una sostituzione, COM non supporta i tipi generici.

Bene, quello era uno sguardo a quello che succede dietro le quinte. Possono essere mari molto scomodi, strani e sconosciuti con i draghi che vivono alla fine della mappa. Può essere molto utile rendere la Terra piatta e modellare un'immagine diversa di ciò che sta realmente accadendo nel codice gestito. Associarlo alla risposta preferita di tutti è comodo in questo modo. Che non funziona così bene per i tipi di valore (non modificare una struttura!) Ma questo è molto ben nascosto. L'errore del metodo GetInterfaceMap () è l'unica perdita nell'astrazione a cui riesco a pensare.


1
Questa è la dichiarazione per la Arrayclasse, che non è il tipo di un array. È il tipo di base per un array. Una matrice monodimensionale in C # non implementa IList<T>. E un tipo non generico può certamente implementare comunque un'interfaccia generica ... che funziona perché ci sono molti tipi diversi - typeof(int[])! = Typeof (string []) , so typeof (int []) `implements IList<int>e typeof(string[])implements IList<string>.
Jon Skeet

2
@ HansPassant: Per favore, non dare per scontato che io abbia votato in negativo su qualcosa solo perché è inquietante . Resta il fatto che sia il tuo ragionamento tramite Array(che, come mostri, è una classe astratta, quindi non può essere il tipo effettivo di un oggetto array) sia la conclusione (che non implementa IList<T>) sono IMO errati. Il modo in cui si implementa IList<T>è insolito e interessante, sono d'accordo, ma è puramente un dettaglio di implementazione . Affermare che T[]non implementa IList<T>è fuorviante IMO. Va contro le specifiche e tutti i comportamenti osservati.
Jon Skeet

6
Beh, sicuramente pensi che non sia corretto. Non puoi farcela con quello che leggi nelle specifiche. Sentiti libero di vederlo a modo tuo, ma non troverai mai una buona spiegazione del perché GetInterfaceMap () fallisce. "Something funky" non è un granché. Indosso occhiali per l'implementazione: ovviamente fallisce, è una digitazione da ciarlatano, un tipo di array concreto in realtà non implementa ICollection <>. Niente di strano. Teniamolo qui, non saremo mai d'accordo.
Hans Passant

4
Che ne dici di rimuovere almeno la logica spuria che afferma che gli array non possono implementare IList<T> perché Array non lo fa? Questa logica è una parte importante di ciò con cui non sono d'accordo. Oltre a ciò, penso che dovremmo essere d'accordo su una definizione di cosa significhi per un tipo implementare un'interfaccia: a mio avviso, i tipi di array mostrano tutte le caratteristiche osservabili dei tipi che implementano IList<T>, oltre a GetInterfaceMapping. Ancora una volta, il modo in cui si ottiene ciò è di minore importanza per me, proprio come mi va bene dire che System.Stringè immutabile, anche se i dettagli di implementazione sono diversi.
Jon Skeet

1
E il compilatore CLI C ++? Quello ovviamente dice "Non ho idea di come farlo!" e genera un errore. Ha bisogno di un cast esplicito IList<T>per funzionare.
Tobias Knauss

21

IList<T>.Countè implementato esplicitamente :

int[] intArray = new int[10];
IList<int> intArrayAsList = (IList<int>)intArray;
Debug.Assert(intArrayAsList.Count == 10);

Questo viene fatto in modo che quando hai una semplice variabile di array, non hai entrambi Counte Lengthdirettamente disponibili.

In generale, l'implementazione esplicita dell'interfaccia viene utilizzata quando si desidera garantire che un tipo possa essere utilizzato in un modo particolare, senza costringere tutti i consumatori del tipo a pensarlo in questo modo.

Modifica : Ops, brutto ricordo lì. ICollection.Countè implementato esplicitamente. Il generico IList<T>è gestito come Hans descrive di seguito .


4
Mi chiedo, però, perché non hanno chiamato la proprietà Conte invece di Lunghezza? Array è l'unica raccolta comune che ha una tale proprietà (a meno che non contiate string).
Tim S.

5
@TimS Una buona domanda (e una di cui non so la risposta.) Direi che il motivo è perché "count" implica un certo numero di elementi, mentre un array ha una "lunghezza" immutabile non appena viene allocato ( indipendentemente da quali elementi hanno valori.)
dlev

1
@TimS Penso che sia fatto perché ICollectiondichiara Count, e sarebbe ancora più confuso se un tipo con la parola "raccolta" non usasse Count:). Ci sono sempre dei compromessi nel prendere queste decisioni.
dlev

4
@ JohnSaunders: E ancora ... solo un voto negativo senza informazioni utili.
Jon Skeet

5
@ JohnSaunders: non sono ancora convinto. Hans ha fatto riferimento all'implementazione SSCLI, ma ha anche affermato che i tipi di array non vengono nemmeno implementati IList<T>, nonostante sia il linguaggio che le specifiche CLI sembrino contrarie. Oserei dire che il modo in cui l'implementazione dell'interfaccia funziona sotto le coperte potrebbe essere complicato, ma questo è il caso in molte situazioni. Sottovaluteresti anche qualcuno che dice che System.Stringè immutabile, solo perché il funzionamento interno è mutevole? Per tutti gli scopi pratici - e certamente per quanto riguarda il linguaggio C # - è esplicito impl.
Jon Skeet


2

Non è diverso da un'implementazione dell'interfaccia esplicita di IList. Solo perché si implementa l'interfaccia non significa che i suoi membri debbano apparire come membri della classe. Si fa implementare la proprietà Count, semplicemente non esporla su X [].


1

Con la disponibilità di fonti di riferimento:

//----------------------------------------------------------------------------------------
// ! READ THIS BEFORE YOU WORK ON THIS CLASS.
// 
// The methods on this class must be written VERY carefully to avoid introducing security holes.
// That's because they are invoked with special "this"! The "this" object
// for all of these methods are not SZArrayHelper objects. Rather, they are of type U[]
// where U[] is castable to T[]. No actual SZArrayHelper object is ever instantiated. Thus, you will
// see a lot of expressions that cast "this" "T[]". 
//
// This class is needed to allow an SZ array of type T[] to expose IList<T>,
// IList<T.BaseType>, etc., etc. all the way up to IList<Object>. When the following call is
// made:
//
//   ((IList<T>) (new U[n])).SomeIListMethod()
//
// the interface stub dispatcher treats this as a special case, loads up SZArrayHelper,
// finds the corresponding generic method (matched simply by method name), instantiates
// it for type <T> and executes it. 
//
// The "T" will reflect the interface used to invoke the method. The actual runtime "this" will be
// array that is castable to "T[]" (i.e. for primitivs and valuetypes, it will be exactly
// "T[]" - for orefs, it may be a "U[]" where U derives from T.)
//----------------------------------------------------------------------------------------
sealed class SZArrayHelper {
    // It is never legal to instantiate this class.
    private SZArrayHelper() {
        Contract.Assert(false, "Hey! How'd I get here?");
    }

    /* ... snip ... */
}

Nello specifico questa parte:

lo stub dispatcher dell'interfaccia lo considera un caso speciale , carica SZArrayHelper, trova il metodo generico corrispondente (abbinato semplicemente al nome del metodo) , lo istanzia per tipo e lo esegue.

(Enfasi mia)

Fonte (scorri verso l'alto).

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.