2010-09-23 19 views
6

Ho scritto un metodo di estensione in csharp per un helper Html MVCContrib e sono rimasto sorpreso dalla forma del vincolo generico, che a prima vista sembra circolare riferimento stesso attraverso il parametro type.Perché questo compilatore generico viene compilato quando sembra avere un riferimento circolare

Ciò detto, il metodo compila e funziona come desiderato.

Mi piacerebbe che qualcuno spiegasse perché questo funziona e se esiste una sintassi intuitiva più intuitiva e se no qualcuno sa perché?

Questo è il codice di compilazione e funzione ma ho rimosso l'elenco di esempio T durante l'offuscamento del problema. e un metodo analogo utilizzando un elenco <T>.

namespace MvcContrib.FluentHtml 
{ 
    public static class FluentHtmlElementExtensions 
    { 
    public static TextInput<T> ReadOnly<T>(this TextInput<T> element, bool value) 
     where T: TextInput<T> 
    { 
     if (value) 
      element.Attr("readonly", "readonly"); 
     else 
      ((IElement)element).RemoveAttr("readonly"); 
     return element; 
    } 
    } 
} 

/*analogous method for comparison*/ 
    public static List<T> AddNullItem<T>(this List<T> list, bool value) 
     where T : List<T> 
    { 
     list.Add(null); 
     return list; 
    } 

Nel primo metodo il vincolo T: TextInput <T> sembra a tutti gli effetti, circolare. Tuttavia, se io commento fuori ottengo un errore di compilazione:

"Il tipo di 'T' non può essere utilizzato come parametro di tipo 'T' nel tipo generico o il metodo 'MvcContrib.FluentHtml.Elements.TextInput <T>' Non c'è conversione di boxe o conversione di parametri di tipo da "T" a "MvcContrib.FluentHtml.Elements.TextInput <T>". "

e nell'elenco delle <T> caso l'errore (s) sono:

"La partita metodo migliore overload per 'System.Collections.Generic.List.Add (T)' ha qualche argomenti non validi argomento 1: impossibile convertire da '<nullo>' a 'T'"

ho potuto immaginare una definizione più intuitiva sarebbe uno che comprende 2 tipi, un riferimento t o il tipo generico e un riferimento di vincoli Tipo es:

public static TextInput<T> ReadOnly<T,U>(this TextInput<T> element, bool value) 
    where U: TextInput<T> 

o

public static U ReadOnly<T,U>(this U element, bool value) 
    where U: TextInput<T> 

ma nessuno di questi compilazione.

+0

Come è stato risposto già questo non è circolare, tuttavia, come una nota a margine è la possibilità di creare ereditarietà circolare che a volte compila e talvolta no (come aggiungere, rimuovere o rinominare file e cartelle può far sì che la compilazione abbia esito positivo o negativo). Quindi esistono bug con eredità circolare. (VS2010) – AnorZaken

risposta

10

AGGIORNAMENTO: questa domanda era la base del mio blog article on the 3rd of February 2011. Grazie per la bella domanda!


Questo è legale, non è circolare, ed è abbastanza comune. Personalmente non mi piace.

Le motivazioni non mi piace che sono:

1) E 'troppo intelligente; come hai scoperto, il codice intelligente è difficile da comprendere per le persone che non conoscono le complessità del sistema di tipi.

2) Non si adatta bene al mio intuito di ciò che un tipo generico "rappresenta". Mi piacciono le classi per rappresentare categorie di cose e classi generiche per rappresentare categorie parametrizzate. Mi è chiaro che una "lista di stringhe" e una "lista di numeri" sono entrambi i tipi di liste, che differiscono solo per il tipo di cosa nella lista. È molto meno chiaro per me cosa sia "un TextInput di T in cui T è un TextInput di T". Non farmi pensare.

3) Questo schema è frequentemente utilizzato nel tentativo di imporre un vincolo nel sistema di tipi che in realtà non è applicabile in C#. Vale a dire questo:

abstract class Animal<T> where T : Animal<T> 
{ 
    public abstract void MakeFriends(IEnumerable<T> newFriends); 
} 
class Cat : Animal<Cat> 
{ 
    public override void MakeFriends(IEnumerable<Cat> newFriends) { ... } 
} 

L'idea qui è "una sottoclasse Gatto animale può solo fare amicizia con gli altri gatti"

Il problema è che la regola desiderata non è effettivamente applicata:

class Tiger: Animal<Cat> 
{ 
    public override void MakeFriends(IEnumerable<Cat> newFriends) { ... } 
} 

Ora una tigre può fare amicizia con i gatti, ma non con le tigri.

per fare effettivamente questo lavoro in C# avresti bisogno di fare qualcosa di simile:

abstract class Animal 
{ 
    public abstract void MakeFriends(IEnumerable<THISTYPE> newFriends); 
} 

dove "THISTYPE" è una nuova caratteristica del linguaggio magico che significa "una classe assoluta è tenuto a compilare il proprio digitare qui".

class Cat : Animal 
{ 
    public override void MakeFriends(IEnumerable<Cat> newFriends) {} 
} 

class Tiger: Animal 
{ 
    // illegal! 
    public override void MakeFriends(IEnumerable<Cat> newFriends) { ... } 
} 

Purtroppo, non è né tipizzate:

Animal animal = new Cat(); 
animal.MakeFriends(new Animal[] {new Tiger()}); 

Se la regola è "un animale può fare amicizia con uno dei suo genere", quindi un animale può fare amicizia con gli animali. Ma un gatto può solo fare amicizia con i gatti, non con le tigri! Il materiale nelle posizioni dei parametri deve essere valido in modo controverso; in questo caso ipotetico avremmo bisogno di covarianza, che non funzionerà.

Mi sembra di aver digerito un po '. Tornando al soggetto di questo modello stranamente ricorrente: provo utilizzare solo questo modello comune, facilmente comprensibile situazioni come quelle citate da altre risposte:

class SortedList<T> where T : IComparable<T> 

Cioè, dobbiamo ogni T sia paragonabile a ogni altri T se abbiamo qualche speranza di fare una lista ordinata di loro.

Per effettivamente essere segnalata come circolare ci deve essere una circolarità buona fede in dipendenze:

class C<T, U> where T : U where U : T 

Un'area interessante della teoria tipo (che attualmente il C# compilatore gestisce malamente) è l'area di non -circolare ma infinito tipi generici. Ho scritto un rilevatore di tipo infinito ma non è stato inserito nel compilatore C# 4 e non rappresenta un'alta priorità per possibili future versioni ipotetiche del compilatore. Se siete interessati ad alcuni esempi di tipi infinitari, o alcuni esempi di dove il rilevatore # ciclo C scombina, vedere il mio articolo su questo:

http://blogs.msdn.com/b/ericlippert/archive/2008/05/07/covariance-and-contravariance-part-twelve-to-infinity-but-not-beyond.aspx

+0

Grazie Eric, è un'area interessante. Ho alcune altre domande sui vincoli ma suppongo che dovrei postarli come un'altra domanda :-). –

1

Il modo in cui lo si utilizza non ha alcun senso. Ma utilizzando un parametro generico in un vincolo sul medesimo parametro è normale, qui un esempio più evidente:

class MySortedList<T> where T : IComparable<T> 

Il vincolo esprime il fatto che ci deve essere un ordinamento tra oggetti di tipo T per metterli nell'ordine ordinato.

EDIT: ho intenzione di decostruire il tuo secondo esempio, dove il vincolo è in realtà sbagliato, ma aiuta compilazione.

Il codice in questione è:

/*analogous method for comparison*/ 
public static List<T> AddNullItem<T>(this List<T> list, bool value) 
    where T : List<T> 
{ 
    list.Add(null); 
    return list; 
} 

Il motivo per cui non si compila senza un vincolo è che i tipi di valore non può essere null. List<T> è un tipo di riferimento, pertanto forzando where T : List<T> si forza T come un tipo di riferimento che può essere nullo. Ma anche fare AddNullItem quasi inutile, dal momento che non è più possibile chiamare il List<string>, ecc Il vincolo corretta è:

/* corrected constraint so the compiler won't complain about null */ 
public static List<T> AddNullItem<T>(this List<T> list) 
    where T : class 
{ 
    list.Add(null); 
    return list; 
} 

NB: ho anche tolto il secondo parametro che era inutilizzato.

Ma si può anche rimuovere tale vincolo se si utilizza default(T), previsto proprio per questo scopo, significa null quando T è un tipo di riferimento e all-zero per qualsiasi tipo di valore.

/* most generic form */ 
public static List<T> AddNullItem<T>(this List<T> list) 
{ 
    list.Add(default(T)); 
    return list; 
} 

ho il sospetto che il primo metodo ha anche bisogno di un vincolo come T : class, ma dal momento che non ho tutte le classi che si sta utilizzando non posso dire per certo.

+0

Sono d'accordo sul fatto che non ha senso, ma compila e fa quello che voglio. Il tuo esempio è troppo semplice per catturare il caso d'uso. –

+0

Mi spiace di premere invio, intendevo continuare ... Sto pensando ad una lista <T> dove potrei avere un elenco di banane e avere un metodo di estensione come list.AddNullItem() così il parametro generico, è esso stesso generico . –

+0

Spero che le informazioni aggiuntive che ho aggiunto ti aiuteranno a capire perché non è stato compilato senza il vincolo, ma il vincolo non è necessariamente corretto. –

0

Posso solo immaginare quale sia il codice che hai pubblicato. Detto questo, posso vedere il merito in un vincolo di tipo generico come questo. Avrebbe senso (per me) in qualsiasi scenario in cui si desidera un argomento di qualche tipo che può eseguire determinate operazioni su argomenti dello stesso tipo.

Ecco un esempio non correlato:

public static IComparable<T> Max<T>(this IComparable<T> value, T other) 
    where T : IComparable<T> 
{ 
    return value.CompareTo(other) > 0 ? value : other; 
} 

codice come questo permetterebbe di scrivere qualcosa di simile a:

int start = 5; 
var max = start.Max(6).Max(3).Max(10).Max(8); // result: 10 

Lo spazio dei nomi FluentHtml è quello che dovrebbe sorta di punta voi fuori che questa è l'intenzione del codice (per abilitare il concatenamento delle chiamate al metodo).

5

Il vincolo di motivo è perché il tipo TextInput ha un tale vincolo come tale.

public abstract class TextInput<T> where T: TextInput<T>{ 
    //... 
} 

noti inoltre che TextInput<T> è estratto e l'unico modo per fare un esempio di tale classe è derivare da esso in modo CRTP simile:

public class FileUpload : TextInput<FileUpload> { 
} 

Il metodo di estensione non si compili senza quel vincolo, ecco perché è lì.

La ragione per avere CRTP in primo luogo è quello di consentire metodi fortemente tipizzati consentendo Fluent Interface sul base di classe, in modo da considerare come esempio:

public abstract class TextInput<T> where T: TextInput<T>{ 
    public T Length(int length) { 
     Attr(length); 
     return (T)this; 
    } 
} 
public class FileUpload : TextInput<FileUpload> { 
    FileUpload FileName(string fileName) { 
     Attr(fileName); 
     return this; 
    } 
} 

Quindi, quando si dispone di un FileUpload esempio, Length rendimenti un'istanza di FileUpload, anche se è definita sulla classe base.Questo rende la seguente sintassi possibili:

FileUpload upload = new FileUpload(); 
upload      //FileUpload instance 
.Length(5)     //FileUpload instance, defined on TextInput<T> 
.FileName("filename.txt"); //FileUpload instance, defined on FileUpload 

EDIT Per affrontare i commenti di OP circa l'ereditarietà di classe ricorsiva. Questo è un modello ben noto in C++ chiamato Pattern modello ricorrente. Leggetelo here. Fino ad oggi non sapevo che fosse possibile in C#. Sospetto che il vincolo abbia qualcosa a che fare con l'abilitazione all'uso di questo modello in C#.

+0

Igor Penso che il tuo commento sul tipo che eredita da un tipo vincolato sta diventando parte del problema che non verrà compilato senza il vincolo. Ma la mia vera domanda riguarda comunque la sintassi, dove T sembra riferirsi sia al tipo che al vincolo, cioè sia TextInput di T che T stesso che sembra ambiguo. –

+0

Grazie appena avuto una lettura del CRTP. Chuckle, il mio esempio ha una sensazione di tipo "questo è dal design", nel senso che è possibile definire la circolarità come ricorsione involontaria, o in questo caso ciò che appare come circolarità è davvero ricorsione. Mostra anche quanto sia difficile diventare fluenti in un determinato linguaggio di programmazione :) –

+0

Haha, ti stavo schiacciando usando il termine fluente. Questo è molto appropriato qui in quanto questo modello è noto come Fluent Interface (metodo di concatenamento). –

0
public static TextInput<T> ReadOnly<T>(this TextInput<T> element, bool value) 
    where T: TextInput<T> 

Diciamo scomposizione:

TextInput<T> è il tipo di ritorno.

TextInput<T> è il tipo essendo esteso (il tipo del primo parametro al metodo statico)

ReadOnly<T> è il nome della funzione che si estende un tipo di cui definizione include T, cioè TextInput<T>.

where T: TextInput<T> è il vincolo T da ReadOnly<T>, tale che T può essere utilizzato in un generico TextInput<TSource>. (T è TSource!)

Non penso che sia circolare.

Se si elimina il vincolo, mi aspetto che element stia cercando di essere convertito nel tipo generico (non un TextInput del tipo generico), che ovviamente non funzionerà.

+0

Ciao Jeff, hai tutte le stesse supposizioni di me tranne che trarre conclusioni diverse. Ci si sente circolare perché utilizzando substition matematico o logico il vincolo implica che dovremmo essere in grado di fare qualcosa del genere: ReadOnly dove T: TextInput => ReadOnly > dove T: TextInput => ReadOnly >> .... sciacquare e ripetere all'infinito :). –

+0

Ciao Simon, se pensi a 'dove T: TextInput ' come dire semplicemente "dove T è la parte generica di TextInput", dovrebbe fare clic. Fiduciosamente. :) –

+0

In altre parole, non leggere la clausola where come sostituzione logica. Non è. Ha lo scopo di definire come T sia correlato ad un altro oggetto, che è più simile alla composizione che alla sostituzione. –

Problemi correlati