2010-10-29 16 views
6

Rivedere un earlier question in SO, ho iniziato a pensare alla situazione in cui una classe espone un valore, come una raccolta, come un'interfaccia implementata dal tipo del valore. Nell'esempio di codice riportato di seguito, sto utilizzando un elenco e esponendo tale elenco come IEnumerable.Trasmissione da interfaccia a tipo sottostante

Esporre la lista tramite l'interfaccia IEnumerable definisce l'intento di elencare solo la lista, non di modificarla. Tuttavia, poiché l'istanza può essere rigirata nuovamente in una lista, la lista stessa può ovviamente essere modificata.

Includo anche nell'esempio una versione del metodo che impedisce la modifica copiando i riferimenti alle voci di elenco in un nuovo elenco ogni volta che viene chiamato il metodo, impedendo così le modifiche all'elenco sottostante.

Quindi la mia domanda è, se tutto il codice che espone un tipo concreto come interfaccia implementata lo fa tramite un'operazione di copia? Ci sarebbe un valore in un costrutto linguistico che indichi esplicitamente "Voglio esporre questo valore attraverso un'interfaccia, e chiamare il codice dovrebbe essere in grado di usare questo valore solo attraverso l'interfaccia"? Quali tecniche usano gli altri per prevenire effetti collaterali indesiderati come questi quando si espongono valori concreti attraverso le loro interfacce.

Nota, capisco che il comportamento illustrato è un comportamento previsto. Non sto affermando che questo comportamento è sbagliato, solo che consente l'uso di funzionalità diverse dall'intenzione espressa. Forse attribuisco troppa importanza all'interfaccia - pensandoci come un vincolo di funzionalità. Pensieri?

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

namespace TypeCastTest 
{ 
    class Program 
    { 
     static void Main(string[] args) 
     { 
      // Demonstrate casting situation 
      Automobile castAuto = new Automobile(); 

      List<string> doorNamesCast = (List<string>)castAuto.GetDoorNamesUsingCast(); 

      doorNamesCast.Add("Spare Tire"); 

      // Would prefer this prints 4 names, 
      // actually prints 5 because IEnumerable<string> 
      // was cast back to List<string>, exposing the 
      // Add method of the underlying List object 
      // Since the list was cast to IEnumerable before being 
      // returned, the expressed intent is that calling code 
      // should only be able to enumerate over the collection, 
      // not modify it. 
      foreach (string doorName in castAuto.GetDoorNamesUsingCast()) 
      { 
       Console.WriteLine(doorName); 
      } 

      Console.WriteLine(); 

      // -------------------------------------- 

      // Demonstrate casting defense 
      Automobile copyAuto = new Automobile(); 

      List<string> doorNamesCopy = (List<string>)copyAuto.GetDoorNamesUsingCopy(); 

      doorNamesCopy.Add("Spare Tire"); 

      // This returns only 4 names, 
      // because the IEnumerable<string> that is 
      // returned is from a copied List<string>, so 
      // calling the Add method of the List object does 
      // not modify the underlying collection 
      foreach (string doorName in copyAuto.GetDoorNamesUsingCopy()) 
      { 
       Console.WriteLine(doorName); 
      } 

      Console.ReadLine(); 
     } 
    } 

    public class Automobile 
    { 
     private List<string> doors = new List<string>(); 
     public Automobile() 
     { 
      doors.Add("Driver Front"); 
      doors.Add("Passenger Front"); 
      doors.Add("Driver Rear"); 
      doors.Add("Passenger Rear"); 
     } 

     public IEnumerable<string> GetDoorNamesUsingCopy() 
     { 
      return new List<string>(doors).AsEnumerable<string>(); 
     } 
     public IEnumerable<string> GetDoorNamesUsingCast() 
     { 
      return doors.AsEnumerable<string>(); 
     } 
    } 

} 
+6

E puoi usare la riflessione per infrangere altre regole e accedere ai membri privati. IMO, lo hai dichiarato come 'IEnumerable'; se qualcuno vuole "imbrogliare" e trasmettere ciò a qualcosa che non hai definito come parte della tua API pubblica, allora è il loro business - e il loro rischio. –

+0

@Kirk Woll - questo è un grande punto sull'uso del riflesso come mezzo per "infrangere le regole". Come parte di questa domanda, volevo il feedback di altre persone sul fatto che la fusione dell'interfaccia con il tipo concreto costituisse anche una forma di "infrangere le regole", o se la maggior parte la considerava un'opzione prevista che dovrebbe essere considerata nella progettazione dell'API. Grazie per il commento. – JeremyDWill

risposta

5

Un modo per evitare questo è quello di utilizzare AsReadOnly() per evitare qualsiasi nefandezza. Penso che la vera risposta sia, comunque, non dovresti mai fare affidamento su qualcosa di diverso dall'interfaccia/contratto esposto in termini di tipi di ritorno, ecc. Fare qualcos'altro sfida l'incapsulamento, ti impedisce di scambiare le tue implementazioni con altri che non lo fanno utilizzare un elenco, ma invece solo un T [], ecc, ecc

Edit:

e giù di fusione come si parla è fondamentalmente una violazione del Liskov Substition Principle, per ottenere tutte le tecniche e roba.

+0

A meno che l'API non sia costruita in modo che il client esegua il cast degli oggetti che vengono restituiti (poiché passano solo il tipo 'object' intorno), il client dovrebbe * MAI * eseguire il cast delle interfacce su tipi concreti. Nel caso di IEnumerable, il codice dovrebbe essere qualcosa come 'var doorNames = new List (castAuto.GetDoorNamesUsingCast());' –

2

In una situazione come questa, è possibile definire la propria classe di raccolta che implementa IEnumerable<T>. Internamente, la vostra collezione potrebbe mantenere un List<T> e poi si può solo tornare l'enumeratore della lista sottostante:

public class MyList : IEnumerable<string> 
{ 
    private List<string> internalList; 

    // ... 

    IEnumerator<string> IEnumerable<string>.GetEnumerator() 
    { 
     return this.internalList.GetEnumerator(); 
    } 

    IEnumerator IEnumerable.GetEnumerator() 
    { 
     return this.internalList.GetEnumerator(); 
    } 
} 
+0

Questo risolve sicuramente il problema specifico nell'esempio e comunica in modo più chiaro l'intento di utilizzo in questo scenario. – JeremyDWill

1

Una cosa che ho imparato a lavorare con .NET (e con alcune persone che sono pronti a saltare a un soluzione di hacking) è che se non altro, la riflessione spesso consente alle persone di passare le "protezioni".

Le interfacce non sono catene di ferro della programmazione, sono una promessa che il tuo codice fa a qualsiasi altro codice che dice "Posso assolutamente fare queste cose". Se "imbroglia" e lanci l'oggetto interfaccia in qualche altro oggetto perché tu, il programmatore, sai qualcosa che il programma non ha, allora stai violando quel contratto. La conseguenza è una minore manutenibilità e una dipendenza dal fatto che nessuno mai incasini qualcosa in quella catena di esecuzione, per timore che qualche altro oggetto venga inviato verso il basso e non trasmesso correttamente.

Altri trucchi come rendere le cose in sola lettura o nascondere l'elenco reale dietro un wrapper sono solo degli stop-gap. Si potrebbe facilmente scavare nel tipo usando il reflection per estrarre la lista privata se davvero lo si desidera.E penso che ci siano attributi che puoi applicare ai tipi per impedire alle persone di rifletterci.

Allo stesso modo, le liste di sola lettura non sono realmente. Potrei probabilmente capire un modo per modificare la lista stessa. E posso quasi certamente modificare gli oggetti sulla lista. Quindi un readonly non è sufficiente, né una copia o un array. Hai bisogno di una copia profonda (clone) dell'elenco originale per poter effettivamente proteggere i dati, in una certa misura.

Ma la vera domanda è: perché stai combattendo così duramente contro il contratto che hai scritto. A volte il reflection hacking è una comoda soluzione alternativa quando la libreria di qualcun altro è progettata male e non espone qualcosa di cui ha bisogno (o un bug richiede che tu debba scavare per risolverlo). Ma quando hai il controllo dell'interfaccia e del consumatore di l'interfaccia, non ci sono scuse per non rendere l'interfaccia esposta pubblicamente robusta quanto è necessario per il tuo lavoro.

In breve: Se è necessario un elenco, non restituire IEnumerable, restituire un elenco. Se hai un IEnumerable ma hai effettivamente bisogno di un elenco, allora è più sicuro creare un nuovo elenco da quel IEnum e usarlo. Ci sono pochissime ragioni (e anche meno, forse no, buone ragioni) per scegliere un tipo semplicemente perché "so che è in realtà una lista, quindi funzionerà".

Sì, puoi prendere provvedimenti per cercare di impedire che le persone lo facciano, ma 1) più ti sforzi di combattere le persone che insistono per rompere il sistema, più difficile tenteranno di romperlo e 2) stanno solo cercando per più corda, e alla fine ne avranno abbastanza per impiccarsi.

2

Un'interfaccia è un vincolo all'implementazione di un insieme minimo di cose che deve fare (anche se "fare" non è altro che lanciare un NotSupportedException o anche un NotImplementedException). Non è un vincolo che impedisce l'implementazione di fare di più, o il codice chiamante.

Problemi correlati