¿Es una buena práctica heredar de tipos genéricos?

35

¿Es mejor usarlo List<string>en anotaciones de tipo o StringListdondeStringList

class StringList : List<String> { /* no further code!*/ }

Me encontré con varios de estos en Irony .

Ruudjah
fuente
Si no heredas de tipos genéricos, ¿por qué están ahí?
Mawg
77
@Mawg Pueden estar disponibles solo para instanciación.
Theodoros Chatzigiannakis
3
Esta es una de mis cosas menos favoritas para ver en el código
Kik
44
Yuck No en serio. ¿Cuál es la diferencia semántica entre StringListy List<string>? No hay ninguno, sin embargo, usted (el "usted" genérico) logró agregar otro detalle a la base del código que es único solo para su proyecto y no significa nada para nadie más hasta después de haber estudiado el código. ¿Por qué enmascarar el nombre de una lista de cadenas solo para seguir llamándola una lista de cadenas? Por otro lado, si quisieras hacerlo BagOBagels : List<string>, creo que tiene más sentido. Puede repetirlo como IEnumerable, los consumidores externos no se preocupan por la implementación: bondad en general.
Craig
1
XAML tiene un problema en el que no puedes usar genéricos y necesitas hacer un tipo como este para completar una lista
Daniel Little

Respuestas:

27

Hay otra razón por la que es posible que desee heredar de un tipo genérico.

Microsoft recomienda evitar anidar tipos genéricos en las firmas de métodos .

Es una buena idea, entonces, crear tipos con nombre de dominio comercial para algunos de sus genéricos. En lugar de tener un IEnumerable<IDictionary<string, MyClass>>, crea un tipo MyDictionary : IDictionary<string, MyClass>y luego tu miembro se convierte en un IEnumerable<MyDictionary>, que es mucho más legible. También le permite usar MyDictionary en varios lugares, lo que aumenta la explicidad de su código.

Esto puede no parecer un gran beneficio, pero en el código comercial del mundo real he visto genéricos que te pondrán los pelos de punta. Cosas como List<Tuple<string, Dictionary<int, List<Dictionary<string, List<double>>>>>. El significado de ese genérico no es de ninguna manera obvio. Sin embargo, si se construyera con una serie de tipos creados explícitamente, la intención del desarrollador original probablemente sería mucho más clara. Aún más, si ese tipo genérico necesita cambiar por alguna razón, encontrar y reemplazar todas las instancias de ese tipo genérico podría ser un verdadero dolor, en comparación con cambiarlo en la clase derivada.

Finalmente, hay otra razón por la cual alguien podría desear crear sus propios tipos derivados de genéricos. Considere las siguientes clases:

public class MyClass
{
    public ICollection<MyItems> MyItems { get; private set; }
    // plumbing code here
}

public class MyOtherClass
{
    public ICollection<MyItems> MyItemCache { get; private set; }
    // plumbing code here
}

public class MyConsumerClass
{
    public MyConsumerClass(ICollection<MyItems> myItems)
    {
        // use the collection
    }
}

Ahora, ¿cómo sabemos qué ICollection<MyItems>pasar al constructor de MyConsumerClass? Si creamos las clases derivadas class MyItems : ICollection<MyItems>y class MyItemCache : ICollection<MyItems>, a continuación, MyConsumerClass puede ser muy específico acerca de cuál de las dos colecciones que realmente quiere. Esto funciona muy bien con un contenedor de IoC, que puede resolver las dos colecciones muy fácilmente. También permite que el programador sea más preciso: si tiene sentido MyConsumerClasstrabajar con cualquier ICollection, puede tomar el constructor genérico. Sin embargo, si nunca tiene sentido comercial utilizar uno de los dos tipos derivados, el desarrollador puede restringir el parámetro constructor al tipo específico.

En resumen, a menudo vale la pena derivar tipos de tipos genéricos: permite un código más explícito cuando sea apropiado y ayuda a la legibilidad y mantenibilidad.

Stephen
fuente
12
Esa es una recomendación de Microsoft absolutamente ridícula.
Thomas Eding
77
No creo que sea tan ridículo para las API públicas. Los genéricos anidados son difíciles de entender de un vistazo y un nombre de tipo más significativo a menudo puede ayudar a la legibilidad.
Stephen
44
No creo que sea ridículo, y ciertamente está bien para API privadas ... pero para API públicas no creo que sea una buena idea. Incluso Microsoft recomienda aceptar y devolver los tipos más básicos al escribir API destinadas al consumo público.
Paul d'Aoust
35

En principio, no hay diferencia entre heredar de tipos genéricos y heredar de cualquier otra cosa.

Sin embargo, ¿por qué se derivaría de alguna clase y no agregaría nada más?

Julia Hayward
fuente
17
Estoy de acuerdo. Sin aportar nada, leía "StringList" y pensaba: "¿Qué hace realmente ?"
Neil
1
También estoy de acuerdo, en muchos idiomas para heredar usas la palabra clave "extiende", este es un buen recordatorio de que si vas a heredar una clase, entonces deberías extenderla con más funcionalidad.
Elliot Blackburn
14
-1, por no entender que esto es sólo una manera de crear una abstracción por la elección de un nombre diferente - la creación de un abstracciones puede ser una muy buena razón para no añadir nada a esta clase.
Doc Brown
1
Considere mi respuesta, analizo algunas de las razones por las que alguien podría decidir hacer esto.
NPSF3000
1
"¿Por qué demonios se derivaría de alguna clase y no agregaría nada más?" Lo he hecho para controles de usuario. Se puede crear un control de usuario genérico, pero no se puede utilizar en una ventana formar diseñador, por lo que tiene que heredar de ella, especificando el tipo, y el uso que uno.
George T
13

No sé C # muy bien, pero creo que esta es la única manera de poner en práctica una typedef, que los programadores de C ++ suelen utilizar por varias razones:

  • Se mantiene el código de parecer un mar de paréntesis angulares.
  • Le permite intercambiar diferentes contenedores subyacentes si sus necesidades cambian más adelante, y deja en claro que se reserva el derecho de hacerlo.
  • Se le permite dar un tipo un nombre que es más relevante en el contexto.

Básicamente descartaron la última ventaja al elegir nombres aburridos y genéricos, pero las otras dos razones siguen vigentes.

Karl Bielefeldt
fuente
77
Eh ... No son exactamente lo mismo. En C ++ tiene un alias literal. StringList es una List<string>- que pueden utilizan indistintamente. Esto es importante cuando se trabaja con otras API (o cuando la exposición de su API), ya que todos los demás en los usos del mundo List<string>.
Telastyn
2
Me doy cuenta de que no son exactamente lo mismo. Tan cerca como está disponible. Es cierto que existen inconvenientes para la versión más débil.
Karl Bielefeldt
24
No, using StringList = List<string>;es el más cercano equivalente C # de un typedef. Crear subclases de esta manera evitará el uso de ningún constructor con argumentos.
Pete Kirkham
44
@PeteKirkham: definir una StringList usingrestringiéndola al archivo de código fuente donde usingse ubica. Desde este punto de vista, la herencia está más cerca typedef. Y crear una subclase no evita que uno agregue todos los constructores necesarios (aunque puede ser tedioso).
Doc Brown
2
@ Random832: permítanme expresar esto de manera diferente: en C ++, se puede usar un typedef para asignar el nombre en un lugar , para que sea una "fuente única de verdad". En C #, usingno se puede usar para este propósito, pero la herencia sí. Esto muestra por qué no estoy de acuerdo con el comentario votado de Pete Kirkham: no lo considero usinguna alternativa real a C ++ typedef.
Doc Brown
9

Se me ocurre al menos una razón práctica.

Declarar tipos no genéricos que heredan de tipos genéricos (incluso sin agregar nada), permite que esos tipos sean referenciados desde lugares donde de otra manera no estarían disponibles, o disponibles de una manera más complicada de lo que deberían ser.

Uno de esos casos es cuando se agregan configuraciones a través del editor de configuraciones en Visual Studio, donde solo los tipos no genéricos parecen estar disponibles. Si va allí, notará que todas las System.Collectionsclases están disponibles, pero no System.Collections.Genericaparecen colecciones según corresponda.

Otro caso es cuando se crean instancias de tipos a través de XAML en WPF. No hay soporte para pasar argumentos de tipo en la sintaxis XML cuando se usa un esquema anterior a 2009. Esto empeora por el hecho de que el esquema de 2006 parece ser el predeterminado para los nuevos proyectos de WPF.

Theodoros Chatzigiannakis
fuente
1
En las interfaces de servicio web de WCF, puede usar esta técnica para proporcionar nombres de tipo de colección más atractivos (por ejemplo, PersonCollection en lugar de ArrayOfPerson o lo que sea)
Rico Suter
7

Creo que necesitas investigar un poco de historia .

  • C # se ha utilizado durante mucho más tiempo del que ha tenido tipos genéricos.
  • Espero que el software dado tenga su propia StringList en algún momento de la historia.
  • Esto bien puede haberse implementado envolviendo una ArrayList.
  • Entonces alguien a refactorizando para mover la base del código hacia adelante.
  • Pero no deseaba tocar 1001 archivos diferentes, así que termine el trabajo.

De lo contrario, Theodoros Chatzigiannakis tuvo las mejores respuestas, por ejemplo, todavía hay muchas herramientas que no entienden los tipos genéricos. O tal vez incluso una mezcla de su respuesta e historia.

Ian
fuente
3
"C # se ha utilizado durante mucho más tiempo del que ha tenido tipos genéricos". En realidad no, C # sin genéricos existió durante aproximadamente 4 años (enero de 2002 - noviembre de 2005), mientras que C # con genéricos ahora tiene 9 años.
svick
4

En algunos casos es imposible usar tipos genéricos, por ejemplo, no puede hacer referencia a un tipo genérico en XAML. En estos casos, puede crear un tipo no genérico que herede del tipo genérico que originalmente quería usar.

Si es posible, lo evitaría.

A diferencia de un typedef, usted crea un nuevo tipo. Si usa StringList como parámetro de entrada a un método, sus llamantes no pueden pasar a List<string>.

Además, necesitaría copiar explícitamente todos los constructores que desea utilizar, en el ejemplo de StringList que no pudo decir new StringList(new[]{"a", "b", "c"}), aunque List<T>define dicho constructor.

Editar:

La respuesta aceptada se centra en los profesionales cuando se utilizan los tipos derivados dentro de su modelo de dominio. Estoy de acuerdo en que potencialmente pueden ser útiles en este caso, aún así, personalmente los evitaría incluso allí.

Pero, en mi opinión, nunca debe usarlos en una API pública porque dificulta la vida de la persona que llama o desperdicia el rendimiento (tampoco me gustan las conversiones implícitas en general).

Lukas Rieger
fuente
3
Usted dice " no puede hacer referencia a un tipo genérico en XAML ", pero no estoy seguro de a qué se refiere. Ver genéricos en XAML
pswg
1
Fue un ejemplo. Sí, es posible con el esquema XAML 2009, pero AFAIK no con el esquema 2006, que sigue siendo el valor predeterminado de VS.
Lukas Rieger
1
Su tercer párrafo es solo cierto a medias, en realidad puede permitir esto con convertidores implícitos;)
Knerd
1
@Knerd, cierto, pero luego 'implícitamente' creas un nuevo objeto. A menos que haga mucho más trabajo, esto también creará una copia de los datos. Probablemente no importa con listas pequeñas, pero diviértete si alguien pasa una lista con unos pocos miles de elementos =)
Lukas Rieger
@LukasRieger tienes razón: PI solo quería señalar eso: P
Knerd
2

Por lo que vale, aquí hay un ejemplo de cómo se usa la herencia de una clase genérica en el compilador Roslyn de Microsoft, y sin siquiera cambiar el nombre de la clase. (Estaba tan desconcertado por esto que terminé aquí en mi búsqueda para ver si esto era realmente posible).

En el proyecto CodeAnalysis puedes encontrar esta definición:

/// <summary>
/// Common base class for C# and VB PE module builder.
/// </summary>
internal abstract class PEModuleBuilder<TCompilation, TSourceModuleSymbol, TAssemblySymbol, TTypeSymbol, TNamedTypeSymbol, TMethodSymbol, TSyntaxNode, TEmbeddedTypesManager, TModuleCompilationState> : CommonPEModuleBuilder, ITokenDeferral
    where TCompilation : Compilation
    where TSourceModuleSymbol : class, IModuleSymbol
    where TAssemblySymbol : class, IAssemblySymbol
    where TTypeSymbol : class
    where TNamedTypeSymbol : class, TTypeSymbol, Cci.INamespaceTypeDefinition
    where TMethodSymbol : class, Cci.IMethodDefinition
    where TSyntaxNode : SyntaxNode
    where TEmbeddedTypesManager : CommonEmbeddedTypesManager
    where TModuleCompilationState : ModuleCompilationState<TNamedTypeSymbol, TMethodSymbol>
{
  ...
}

Luego, en el proyecto CSharpCodeanalysis existe esta definición:

internal abstract class PEModuleBuilder : PEModuleBuilder<CSharpCompilation, SourceModuleSymbol, AssemblySymbol, TypeSymbol, NamedTypeSymbol, MethodSymbol, SyntaxNode, NoPia.EmbeddedTypesManager, ModuleCompilationState>
{
  ...
}

Esta clase PEModuleBuilder no genérica se usa ampliamente en el proyecto CSharpCodeanalysis, y varias clases en ese proyecto heredan de ella, directa o indirectamente.

Y luego en el proyecto BasicCodeanalysis existe esta definición:

Partial Friend MustInherit Class PEModuleBuilder
    Inherits PEModuleBuilder(Of VisualBasicCompilation, SourceModuleSymbol, AssemblySymbol, TypeSymbol, NamedTypeSymbol, MethodSymbol, SyntaxNode, NoPia.EmbeddedTypesManager, ModuleCompilationState)

Dado que podemos (con suerte) suponer que Roslyn fue escrita por personas con un amplio conocimiento de C # y cómo debe usarse, creo que esta es una recomendación de la técnica.

RenniePet
fuente
Sí. Y también se pueden usar para refinar la cláusula where directamente cuando se declara.
Sentinel
1

Hay tres posibles razones por las cuales alguien trataría de hacer eso de la cabeza:

1) Para simplificar las declaraciones:

Claro, StringListy ListStringtienen aproximadamente la misma longitud, pero imagina que estás trabajando con un ejemplo genérico en UserCollectionrealidad Dictionary<Tuple<string,Type>, IUserData<Dictionary,MySerializer>>o con otro.

2) Para ayudar a AOT:

A veces necesita usar la compilación Ahead Of Time, no Just In Time Compilation, por ejemplo, para iOS. A veces, el compilador AOT tiene dificultades para descubrir todos los tipos genéricos con anticipación, y esto podría ser un intento de proporcionarle una pista.

3) Para agregar funcionalidad o eliminar dependencia:

Tal vez tienen una funcionalidad específica que quieren poner en práctica para StringList(podría estar oculto en un método de extensión, no se ha hecho, o histórico) que, o bien no se pueden añadir a la List<string>sin heredar de ella, o que no quieren contaminar List<string>con .

Alternativamente, pueden desear cambiar la implementación de StringListabajo de la pista, por lo que acaban de usar la clase como marcador por ahora (por lo general, haría / usaría interfaces para esto, por ejemplo IList).


También señalaría que ha encontrado este código en Irony, que es un analizador de idiomas, un tipo de aplicación bastante inusual. Puede haber alguna otra razón específica por la que se usó esto.

Estos ejemplos demuestran que puede haber razones legítimas para escribir tal declaración, incluso si no es demasiado común. Al igual que con cualquier cosa, considere varias opciones y elija la que sea mejor para usted en ese momento.


Si echa un vistazo al código fuente , parece que la opción 3 es la razón: usan esta técnica para ayudar a agregar funcionalidad / especializar colecciones:

public class StringList : List<string> {
    public StringList() { }
    public StringList(params string[] args) {
      AddRange(args);
    }
    public override string ToString() {
      return ToString(" ");
    }
    public string ToString(string separator) {
      return Strings.JoinStrings(separator, this);
    }
    //Used in sorting suffixes and prefixes; longer strings must come first in sort order
    public static int LongerFirst(string x, string y) {
      try {//in case any of them is null
        if (x.Length > y.Length) return -1;
      } catch { }
      if (x == y) return 0;
      return 1; 
    }
NPSF3000
fuente
1
A veces, la búsqueda para simplificar las declaraciones (o simplificar lo que sea) puede resultar en una mayor complejidad, en lugar de una menor complejidad. Todos los que conocen el idioma moderadamente bien saben lo que List<T>es, pero tienen que inspeccionar StringListen contexto para comprender realmente lo que es. En realidad, es solo List<string>, ir por un nombre diferente. Realmente no parece haber ninguna ventaja semántica para hacer esto en un caso tan simple. En realidad, solo está reemplazando un tipo conocido con el mismo tipo con un nombre diferente.
Craig
@ Craig Estoy de acuerdo, como lo señaló mi explicación del caso de uso (declaraciones más largas) y otras posibles razones.
NPSF3000
-1

Yo diría que no es una buena práctica.

List<string>comunica "una lista genérica de cadenas". Claramente se List<string> aplican métodos de extensión. Se requiere menos complejidad para entender; solo se necesita la lista.

StringListcomunica "la necesidad de una clase separada". Quizás se List<string> apliquen métodos de extensión (verifique la implementación del tipo real para esto). Se necesita más complejidad para entender el código; Se requiere una lista y el conocimiento de implementación de clase derivado.

Ruudjah
fuente
44
Los métodos de extensión se aplican de todos modos.
Theodoros Chatzigiannakis
2
Por supuesto. Pero no sabe eso hasta que inspeccione la StringList y vea que se deriva de ella List<string>.
Ruudjah
@TheodorosChatzigiannakis Me pregunto si Ruudjah podría explicar qué quiere decir con "los métodos de extensión no se aplican". Los métodos de extensión son una posibilidad con cualquier clase. Claro, la biblioteca de .NET Framework se entrega con muchos métodos de extensión gratuitos llamados LINQ que se aplican a las clases de colección genéricas, pero eso no significa que los métodos de extensión no se apliquen a ninguna otra clase. ¿Quizás Ruudjah está haciendo un punto que no es obvio a primera vista? ;-)
Craig
"los métodos de extensión no se aplican". ¿Dónde lees esto? Estoy diciendo "métodos de extensión pueden aplicar.
Ruudjah
1
Sin embargo, la implementación de tipo no le dirá si se aplican o no los métodos de extensión, ¿verdad? Sólo el hecho de que se trata de una extensión promedio de la clase métodos no se aplican. Cualquier consumidor de su tipo puede crear métodos de extensión completamente independientes de su código. Supongo que a eso estoy llegando o preguntando. ¿Estás hablando de LINQ? LINQ se implementa utilizando métodos de extensión. Pero la ausencia de métodos de extensión específicos de LINQ para un tipo particular no significa que los métodos de extensión para ese tipo no existan o no existan. Podrían ser creados en cualquier momento por cualquier persona.
Craig