2010-09-23 17 views
6

He escrito un método de extensión en csharp para un helper MVCContrib Html y me sorprendió la forma de la restricción genérica, que a primera vista parece circularmente referencia a sí mismo a través del parámetro tipo.¿Por qué esta compilación genérica se compila cuando parece tener una referencia circular

Dicho esto, el método se compila y funciona como se desee.

Me gustaría que alguien explicara por qué funciona esto y si existe una sintaxis intuitiva más intuitiva y si no, si alguien sabe por qué?

Aquí está el código de compilación y función, pero he eliminado el ejemplo de la Lista de T, ya que nubló el problema. , así como un método análogo utilizando una lista <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; 
    } 

En el primer método de la restricción T: T TextInput < > parece a todos los efectos, circular. Sin embargo, si comento a cabo consigo un error del compilador:

"El tipo 'T' no se puede utilizar como parámetro de tipo 'T' en el tipo genérico o método 'MvcContrib.FluentHtml.Elements.TextInput <T>' No hay conversión de cuadro ni conversión de parámetro de tipo de 'T' a 'MvcContrib.FluentHtml.Elements.TextInput <T>'. "

y en la lista <T> caso el error (s) son:

"El partido mejor método sobrecargado para 'System.Collections.Generic.List.Add (T)' tiene alguna argumentos no válidos argumento 1: no se puede convertir de '<nula>' a 'T'"

me podía imaginar una definición más intuitiva sería uno que incluye 2 tipos, una referencia t o el tipo genérico y una referencia a la Limitar a tipo por ejemplo:

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> 

pero ninguno de estos compilación.

+0

Como ha sido contestada ya que esto no es circular, sin embargo, como una nota al margen es que es posible crear una herencia circular que a veces compila y otras veces no (como al agregar, eliminar o renombrar archivos y carpetas, la compilación puede tener éxito al azar o fallar). Así que existen errores con herencia circular. (VS2010) – AnorZaken

Respuesta

10

ACTUALIZACIÓN: Esta pregunta fue la base de mi blog article on the 3rd of February 2011. Gracias por la gran pregunta!


Esto es legal, no es circular, y es bastante común. Personalmente no me gusta.

Las razones No me gusta que son:

1) Es demasiado inteligente; como ha descubierto, el código inteligente es difícil de entender intuitivamente para las personas que no están familiarizadas con las complejidades del sistema de tipos.

2) No se ajusta bien a mi intuición de lo que un tipo genérico "representa". Me gustan las clases para representar categorías de cosas y las clases genéricas para representar categorías parametrizadas. Es claro para mí que una "lista de cadenas" y una "lista de números" son ambos tipos de listas, que difieren solo en el tipo de la cosa en la lista. Es mucho menos claro para mí qué es "una entrada de texto de T donde T es una entrada de texto de T". No me hagas pensar

3) Este patrón se utiliza con frecuencia en un intento de imponer una restricción en el sistema de tipos que en realidad no es aplicable en C#. A saber éste:

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) { ... } 
} 

La idea aquí es "un gato subclase de Animal sólo puede hacer amigos con otros gatos"

El problema es que la norma deseada no se ejecutan en la práctica:

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

Ahora, un tigre puede hacer amigos con los gatos, pero no con tigres.

para hacer realidad esta obra en C# que había necesidad de hacer algo como:

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

donde "THISTYPE" es una nueva característica del lenguaje mágico que significa "se requiere una clase primordial para llenar en su propio escriba aquí".

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

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

Por desgracia, eso no es Typesafe ya sea:

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

Si la regla es "un animal puede hacer amigos con cualquiera de su tipo", entonces un animal puede hacer amigos con los animales. ¡Pero un gato solo puede hacer amigos con gatos, no con tigres! El material en las posiciones de los parámetros tiene que ser válido contravariante; en este caso hipotético, necesitaríamos covarianza, lo cual no funcionará.

Parece que me he desviado un poco. Volviendo al tema de este patrón curiosamente recurrente: Trato de usar solamente este patrón para el común, fácil de entender situaciones como las mencionadas por otras respuestas:

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

Es decir, necesitamos que cada T para ser comparable a cada otra T si tenemos alguna esperanza de hacer una lista ordenada de ellos.

Para realmente quiere marcar como circular que tiene que haber una circularidad de buena fe en las dependencias:

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

Un área interesante de la teoría de tipos (que en la actualidad el compilador de C# maneja mal) es el área de la no -circular pero infinitary tipos genéricos. He escrito un detector de tipo infinitario pero no entró en el compilador C# 4 y no es una alta prioridad para posibles versiones hipotéticas futuras del compilador. Si usted está interesado en algunos ejemplos de tipos infinitary, o algunos ejemplos en los que el detector # ciclo C meta la pata, ver mi artículo sobre que:

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

+0

Gracias Eric, es un área interesante. Tengo algunas otras preguntas sobre las limitaciones, pero creo que debería publicarlas como otra pregunta :-). –

1

La forma en que lo está utilizando no tiene ningún sentido. Pero el uso de un parámetro genérico en una restricción en ese mismo parámetro es bastante normal, aquí está un ejemplo más obvio:

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

La restricción expresa el hecho de que debe haber un ordenamiento entre los objetos de tipo T con el fin de ponerlos en orden ordenado

EDITAR: Voy a deconstruir su segundo ejemplo, donde la restricción es realmente incorrecta pero lo ayuda a compilar.

El código en cuestión es:

/*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; 
} 

La razón de que no se compilará sin restricción es que los tipos de valor no pueden ser null. List<T> es un tipo de referencia, por lo que forzando where T : List<T> fuerza T para que sea un tipo de referencia que puede ser nulo. Pero también se hace AddNullItem casi inútil, puesto que ya no se puede llamar en List<string>, etc. La restricción correcta es:

/* 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: Yo también eliminó el segundo parámetro, que no estaba acostumbrado.

Pero incluso se puede eliminar esa restricción si se utiliza default(T), que se proporciona precisamente para este propósito, significa null cuando T es un tipo de referencia y todo ceros para cualquier tipo de valor.

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

que sospecha que su primer método también necesita una restricción como T : class, pero como yo no tengo todas las clases que está utilizando no puedo decir con certeza.

+0

Estoy de acuerdo en que no tiene sentido, pero compila y hace lo que yo quiero. Su ejemplo es demasiado simple para capturar el caso de uso. –

+0

Lo siento, presiono enter, quise continuar ... Estoy pensando en una lista <T> donde podría tener una lista de plátanos y tener un método de extensión como list.AddNullItem() para que el parámetro genérico sea en sí mismo genérico . –

+0

Espero que la información adicional que agregué te ayudará a ver por qué no compiló sin la restricción, pero la restricción no es necesariamente correcta. –

0

Solo puedo adivinar qué hace el código que ha publicado. Dicho esto, puedo ver el mérito en una restricción de tipo genérico como este. Tendría sentido (para mí) en cualquier escenario en el que desee un argumento de algún tipo que pueda realizar ciertas operaciones en argumentos del mismo tipo.

He aquí un ejemplo relacionado:

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

código como esto permitirá escribir algo como:

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

El espacio de nombres FluentHtml es qué tipo de debe dar una propina de que esta es la intención del código (para habilitar el encadenamiento de llamadas a métodos).

5

La restricción de razón es que existe porque el tipo TextInput tiene una restricción como esta.

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

También tenga en cuenta que TextInput<T> es abstracto y la única manera de hacer que una instancia de dicha clase es derivar de ella de una manera CRTP-como:

public class FileUpload : TextInput<FileUpload> { 
} 

El método de extensión no se compilará sin esa restricción, es por eso que está ahí.

La razón de tener CRTP en el primer lugar es para permitir métodos inflexible de tipos que permite Fluent Interface en la clase base de, por lo que considerar como ejemplo:

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; 
    } 
} 

Así que cuando usted tiene un FileUpload ejemplo, Length retornos una instancia de FileUpload, aunque esté definida en la clase base.Esto hace posible la siguiente sintaxis:

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

EDITAR Para hacer frente a los comentarios de OP sobre la herencia de clases recursiva. Este es un patrón bien conocido en C++ llamado Curiously Recurring Template Pattern. Lee esto here. Hasta hoy no sabía que era posible en C#. Sospecho que la restricción tiene algo que ver con habilitar el uso de este patrón en C#.

+0

Igor Creo que su comentario sobre el tipo que hereda de un tipo restringido es obtener es parte del problema que no se compilará sin la restricción. Pero mi verdadera pregunta es acerca de la sintaxis, donde T parece referirse tanto al tipo como a la restricción, es decir, tanto la entrada de texto de T como la de T, que parece ambigua. –

+0

Gracias acaba de leer el CRTP. Risita, mi ejemplo tiene una sensación de tipo "esto es por diseño", en el sentido de que puedes definir la circularidad como recursividad involuntaria, o en este caso lo que parece circularidad es realmente recursividad. También muestra lo difícil que es llegar a ser fluido en un lenguaje de programación dado :) –

+0

Jaja, te estaba tirando usando el término con fluidez. Eso es muy apropiado aquí ya que este patrón se conoce como Interfaz fluida (encadenamiento de métodos). –

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

Vamos a desglosarlo:

TextInput<T> es el tipo de retorno.

TextInput<T> es el tipo que se está extendida (del tipo de la primera parámetro para el método estático)

ReadOnly<T> es el nombre de la función que se extiende un tipo cuya definición incluye T, es decir TextInput<T>.

where T: TextInput<T> es la restricción en T desde ReadOnly<T>, de modo que T se puede utilizar en un genérico TextInput<TSource>. (¡Es TSource!)

No creo que sea circular.

Si elimina la restricción, esperaría que element intente convertirse al tipo genérico (no a un TextInput del tipo genérico), lo que obviamente no va a funcionar.

+0

Hola Jeff, tienes todas las mismas suposiciones que yo, excepto que sacamos conclusiones diferentes. Se siente circular para mí porque el uso SUSTITUCIÓN matemática o lógica la restricción implica que debemos ser capaces de hacer algo como esto: sólo lectura donde T: TextInput => de sólo lectura > donde T: TextInput => Sólo lectura < TextInput >> .... enjuague y repita ad infinitum :). –

+0

Hola Simon, si piensas en 'donde T: TextInput ' como diciendo "donde T es la parte genérica de TextInput", debería hacer clic. Ojalá. :) –

+0

En otras palabras, no lea la cláusula where como sustitución lógica. No es. Está destinado a definir cómo T está relacionado con otro objeto, que se parece más a la composición que a la sustitución. –

Cuestiones relacionadas