Generics vs interfaccia comune?


20

Non ricordo quando ho scritto la lezione generica l'ultima volta. Ogni volta che penso di averne bisogno dopo aver pensato che faccio una conclusione, non lo faccio.

La seconda risposta a questa domanda mi ha fatto chiedere chiarimenti (dal momento che non posso ancora commentare, ho fatto una nuova domanda).

Quindi prendiamo il codice dato come esempio del caso in cui uno ha bisogno di generici:

public class Repository<T> where T : class, IBusinessOBject
{
  T Get(int id)
  void Save(T obj);
  void Delete(T obj);
}

Ha vincoli di tipo: IBusinessObject

Il mio solito modo di pensare è: la classe è costretta a usare IBusinessObject, così come lo sono le classi che la usano Repository. Il repository memorizza questi messaggi IBusinessObject, molto probabilmente i clienti Repositoryvorranno ottenere e usare oggetti attraverso l' IBusinessObjectinterfaccia. Quindi perché non solo

public class Repository
{
  IBusinessOBject Get(int id)
  void Save(IBusinessOBject obj);
  void Delete(IBusinessOBject obj);
}

Tuttavia, l'esempio non è buono, in quanto è solo un altro tipo di raccolta e la raccolta generica è classica. Anche in questo caso il vincolo di tipo sembra strano.

In effetti l'esempio class Repository<T> where T : class, IBusinessbBjectè molto simile class BusinessObjectRepositorya me. Qual è la cosa che i generici sono fatti per risolvere.

Il punto è: i generici sono buoni per qualsiasi cosa tranne le raccolte e i vincoli di tipo non rendono generico quanto specializzato, come fa l'uso di questo vincolo di tipo invece del parametro di tipo generico all'interno della classe?

Risposte:


24

Parliamo prima del polimorfismo parametrico puro e in seguito entriamo nel polimorfismo limitato.

Polimorfismo parametrico

Cosa significa polimorfismo parametrico? Bene, significa che un tipo, o meglio un costruttore di tipi, è parametrizzato da un tipo. Poiché il tipo viene passato come parametro, non è possibile sapere in anticipo quale potrebbe essere. Non è possibile formulare ipotesi basate su di esso. Ora, se non sai cosa potrebbe essere, a che serve? Cosa puoi farci?

Bene, potresti archiviarlo e recuperarlo, per esempio. È il caso che hai già menzionato: le raccolte. Per archiviare un elemento in un elenco o un array, non ho bisogno di sapere nulla sull'elemento. L'elenco o l'array può essere completamente ignaro del tipo.

Ma per quanto riguarda il Maybetipo? Se non lo conosci, Maybeè un tipo che forse ha un valore e forse no. Dove lo useresti? Bene, ad esempio, quando si estrae un elemento da un dizionario: il fatto che un elemento potrebbe non essere nel dizionario non è una situazione eccezionale, quindi non si dovrebbe davvero fare un'eccezione se l'elemento non è presente. Invece, si restituisce un'istanza di un sottotipo di Maybe<T>, che ha esattamente due sottotipi: Nonee Some<T>. int.Parseè un altro candidato di qualcosa che dovrebbe davvero restituire un Maybe<int>invece di lanciare un'eccezione o l'intera int.TryParse(out bla)danza.

Ora, potresti sostenere che Maybeè un po 'come una lista che può contenere solo zero o uno elementi. E quindi una specie di collezione.

E allora Task<T>? È un tipo che promette di restituire un valore ad un certo punto in futuro, ma non ha necessariamente un valore in questo momento.

O che dire Func<T, …>? Come rappresenteresti il ​​concetto di una funzione da un tipo a un altro se non riesci ad astrarre sui tipi?

O, più in generale: considerando che l'astrazione e il riutilizzo sono le due operazioni fondamentali dell'ingegneria del software, perché non dovresti voler astrarre sui tipi?

Polimorfismo limitato

Quindi, parliamo ora del polimorfismo limitato. Il polimorfismo limitato è fondamentalmente il punto in cui il polimorfismo parametrico e il polimorfismo del sottotipo si incontrano: invece di un costruttore di tipi che è completamente ignaro del suo parametro di tipo, puoi legare (o vincolare) il tipo a un sottotipo di un tipo specificato.

Torniamo alle collezioni. Prendi una hashtable. Abbiamo detto sopra che un elenco non ha bisogno di sapere nulla dei suoi elementi. Bene, una hashtable lo fa: deve sapere che può hash. (Nota: in C #, tutti gli oggetti sono hash, proprio come tutti gli oggetti possono essere confrontati per uguaglianza. Questo non è vero per tutte le lingue, tuttavia, ed è talvolta considerato un errore di progettazione anche in C #.)

Pertanto, si desidera vincolare il parametro type per il tipo chiave nella tabella hash in modo che sia un'istanza di IHashable:

class HashTable<K, V> where K : IHashable
{
  Maybe<V> Get(K key);
  bool Add(K key, V value);
}

Immagina se invece avessi questo:

class HashTable
{
    object Get(IHashable key);
    bool Add(IHashable key, object value);
}

Cosa faresti con un valueche esci di lì? Non puoi farci niente, sai solo che è un oggetto. E se iteri su di esso, tutto ciò che ottieni è una coppia di qualcosa che sai essere IHashable(che non ti aiuta molto perché ha solo una proprietà Hash) e qualcosa che conosci è un object(che ti aiuta anche meno).

O qualcosa basato sul tuo esempio:

class Repository<T> where T : ISerializable
{
    T Get(int id);
    void Save(T obj);
    void Delete(T obj);
}

L'articolo deve essere serializzabile perché verrà archiviato sul disco. Ma cosa succede se hai questo invece:

class Repository
{
    ISerializable Get(int id);
    void Save(ISerializable obj);
    void Delete(ISerializable obj);
}

Con il caso generico, se si mette un BankAccountin, si ottiene un BankAccountindietro, con metodi e immobili come Owner, AccountNumber, Balance, Deposit, Withdraw, ecc Qualcosa che si può lavorare. Ora l'altro caso? Hai messo in una BankAccountma si ottiene indietro un Serializable, che ha solo una proprietà: AsString. Che cosa hai intenzione di fare con quello?

Ci sono anche alcuni trucchi che puoi fare con il polimorfismo limitato:

Polimorfismo limitato da F.

La quantificazione limitata da F è fondamentalmente il punto in cui la variabile type appare nuovamente nel vincolo. Questo può essere utile in alcune circostanze. Ad esempio, come si scrive ICloneableun'interfaccia? Come si scrive un metodo in cui il tipo restituito è il tipo della classe di implementazione? In una lingua con una funzionalità MyType , è facile:

interface ICloneable
{
    public this Clone(); // syntax I invented for a MyType feature
}

In una lingua con polimorfismo limitato, puoi invece fare qualcosa del genere:

interface ICloneable<T> where T : ICloneable<T>
{
    public T Clone();
}

class Foo : ICloneable<Foo>
{
    public Foo Clone()
    {
        // …
    }
}

Si noti che questo non è sicuro come la versione MyType, perché non c'è nulla che impedisce a qualcuno di passare semplicemente la classe "sbagliata" al costruttore del tipo:

class EvilBar : ICloneable<SomethingTotallyUnrelatedToBar>
{
    public SomethingTotallyUnrelatedToBar Clone()
    {
        // …
    }
}

Membri di tipo astratto

A quanto pare, se si hanno membri e sottotipi di tipo astratti, si può effettivamente cavarsela completamente senza polimorfismo parametrico e fare comunque le stesse cose. Scala sta andando in questa direzione, essendo il primo linguaggio principale che è iniziato con i generici e poi ha cercato di rimuoverli, che è esattamente il contrario da Java e C #.

Fondamentalmente, in Scala, proprio come puoi avere campi, proprietà e metodi come membri, puoi anche avere tipi. E proprio come campi e proprietà e metodi possono essere lasciati astratti per essere implementati in una sottoclasse in un secondo momento, anche i membri del tipo possono essere lasciati astratti. Torniamo alle raccolte, una semplice List, che sarebbe simile a questa, se fosse supportata in C #:

class List
{
    T; // syntax I invented for an abstract type member
    T Get(int index) { /* … */ }
    void Add(T obj) { /* … */ }
}

class IntList : List
{
    T = int;
}
// this is equivalent to saying `List<int>` with generics

capisco che l'astrazione sui tipi è utile. semplicemente non ne vedo l'uso nella "vita reale". Func <> e Task <> e Action <> sono tipi di libreria. e grazie anche me ne sono ricordato interface IFoo<T> where T : IFoo<T>. questa è ovviamente l'applicazione della vita reale. l'esempio è fantastico. ma per qualche motivo non mi sento soddisfatto. preferisco concentrarmi su quando è appropriato e quando non lo è. le risposte qui danno un contributo a questo processo, ma mi sento ancora incerto su tutto questo. è strano perché i problemi a livello di lingua non mi disturbano da così tanto tempo.
jungle_mole il

Ottimo esempio Mi sembrava di essere tornato in classe. +1
Chef_Code

1
@Chef_Code: spero che sia un complimento :-P
Jörg W Mittag,

Sì. In seguito ho pensato a come poteva essere percepito dopo aver già commentato. Quindi per confermare la sincerità ... Sì, è un complimento nient'altro.
Chef_Code,

14

Il punto è: i generici sono buoni per qualsiasi cosa tranne le raccolte e i vincoli di tipo non rendono generico quanto specializzato, come fa l'uso di questo vincolo di tipo invece del parametro di tipo generico all'interno della classe?

No. Stai pensando troppo Repository, dove è praticamente lo stesso. Ma non è per questo che i generici sono lì. Sono lì per gli utenti .

Il punto chiave qui non è che il Repository stesso sia più generico. È che gli utenti sono più specializzati, cioè quelli Repository<BusinessObject1>e Repository<BusinessObject2>sono di tipi diversi, e inoltre, se prendo un Repository<BusinessObject1>, so che uscirò di BusinessObject1nuovo Get.

Non puoi offrire questa tipizzazione forte dalla semplice eredità. La classe di repository proposta non fa nulla per impedire alle persone di confondere i repository per diversi tipi di oggetti business o di garantire il ritorno del tipo corretto di business object.


Grazie, ha senso. Ma lo scopo di utilizzare questa funzionalità linguistica molto apprezzata è semplice come aiutare gli utenti a cui IntelliSense è spento? (Sto esagerando un po ', ma sono sicuro che
capirai

@zloidooraque: anche IntelliSense non sa che tipo di oggetti sono memorizzati in un repository. Ma sì, potresti fare qualsiasi cosa senza generici se invece sei disposto a usare i cast.
gexicide,

@gexicide questo è il punto: non vedo dove devo usare i cast se uso l'interfaccia comune. Non ho mai detto "uso Object". Inoltre capisco perché usare i generici quando si scrivono raccolte (principio DRY). Probabilmente, la mia domanda iniziale avrebbe dovuto essere qualcosa sull'uso dei generici al di fuori del contesto delle raccolte ..
jungle_mole,

@zloidooraque: non ha nulla a che fare con l'ambiente. Intellisense non può dirti se an IBusinessObjectè a BusinessObject1o a BusinessObject2. Non è in grado di risolvere i sovraccarichi in base al tipo derivato che non conosce. Non può rifiutare il codice che passa nel tipo sbagliato. Ci sono milioni di bit della digitazione più forte di cui Intellisense non può assolutamente fare nulla. Un migliore supporto degli utensili è un bel vantaggio, ma in realtà non ha nulla a che fare con le ragioni principali.
DeadMG

@DeadMG e questo è il mio punto: intellisense non può farlo: usa il generico, quindi potrebbe? importa? quando ottieni l'oggetto dalla sua interfaccia, perché lo abbassa? se lo fai, è un cattivo design, no? e perché e che cosa è "risolvere i sovraccarichi"? l'utente non deve decidere se chiamare il metodo o meno in base al tipo derivato se delega la chiamata del metodo giusto al sistema (quale polimorfismo è). e questo mi porta ancora a una domanda: i generici sono utili al di fuori dei container? non sto litigando con te, ho davvero bisogno di capirlo.
jungle_mole,

13

"molto probabilmente i clienti di questo repository vorranno ottenere e usare oggetti attraverso l'interfaccia IBusinessObject".

No, non lo faranno.

Consideriamo che IBusinessObject ha la seguente definizione:

public interface IBusinessObject
{
  public int Id { get; }
}

Definisce semplicemente l'ID perché è l'unica funzionalità condivisa tra tutti gli oggetti business. E hai due veri e propri oggetti commerciali: Persona e Indirizzo (poiché le Persone non hanno strade e Indirizzi non hanno nomi che non puoi vincolare entrambi a un'interfaccia comune con funzionalità di entrambi. Sarebbe un progetto terribile, violando il Interface Segragation Principle , la "I" in SOLID )

public class Person : IBusinessObject
{
  public int Id { get; private set; }
  public string Name { get; private set; }
}

public class Address : IBusinessObject
{
  public int Id { get; private set; }
  public string City { get; private set; }
  public string StreetName { get; private set; }
  public int Number { get; private set; }
}

Ora, cosa succede quando si utilizza la versione generica del repository:

public class Repository<T> where T : class, IBusinessObject
{
  T Get(int id)
  void Save(T obj);
  void Delete(T obj);
}

Quando si chiama il metodo Get sul repository generico, l'oggetto restituito verrà fortemente digitato, consentendo di accedere a tutti i membri della classe.

Person p = new Repository<Person>().Get(1);
int id = p.Id;
string name = p.Name;

Address a = new Repository<Address>().Get(1);
int id = a.Id;
string cityName = a.City;
int houseNumber = a.Number;

D'altra parte, quando si utilizza il repository non generico:

public class Repository
{
  IBusinessOBject Get(int id)
  void Save(IBusinessOBject obj);
  void Delete(IBusinessOBject obj);
}

Sarai in grado di accedere ai membri solo dall'interfaccia IBusinessObject:

IBussinesObject p = new Repository().Get(1);
int id = p.Id; //OK
string name = p.Name; //Oooops, you dont have "Name" defined on the IBussinesObject interface.

Pertanto, il codice precedente non verrà compilato a causa delle seguenti righe:

string name = p.Name;
string cityName = a.City;
int houseNumber = a.Number;

Certo, puoi lanciare IBussinesObject nella classe reale, ma perderai tutta la magia del tempo di compilazione che i generici consentono (portando a InvalidCastExceptions lungo la strada), soffriranno inutilmente il cast in testa ... E anche se non lo fai preoccupati del tempo di compilazione non controllando nessuna delle prestazioni (dovresti), il casting dopo non ti darà sicuramente alcun vantaggio rispetto all'uso dei generici in primo luogo.

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.