¿Por qué no heredar de List??

Al planificar mis programas, a menudo comienzo con una cadena de pensamientos como esta:

A football team is just a list of football players. Therefore, I should represent it with:

var football_team = new List<FootballPlayer>();

The ordering of this list represent the order in which the players are listed in the roster.

Pero luego me doy cuenta de que los equipos también tienen otras propiedades, además de la mera lista de jugadores, que hay que registrar. Por ejemplo, el total acumulado de puntuaciones de esta temporada, el presupuesto actual, los colores del uniforme, una stringrepresentación del nombre del equipo, etc.

Entonces pienso:

Okay, a football team is just like a list of players, but additionally, it has a name (a string) and a running total of scores (an int). .NET does not provide a class for storing football teams, so I will make my own class. The most similar and relevant existing structure is List<FootballPlayer>, so I will inherit from it:

class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal 
}

Pero resulta que una guía dice que no debes heredarList<T> . Estoy completamente confundido por esta guía en dos aspectos.

¿Por qué no?

Aparentemente Listestá optimizado de alguna manera para el rendimiento . ¿Cómo es eso? ¿Qué problemas de rendimiento causaré si extiendo List? ¿Qué se romperá exactamente?

Otra razón que he visto es que la Listproporciona Microsoft y no tengo control sobre ella, por lo que no puedo cambiarla más tarde, después de exponer una "API pública" . Pero lucho por entender esto. ¿Qué es una API pública y por qué debería importarme? Si mi proyecto actual no tiene y es probable que nunca tenga esta API pública, ¿puedo ignorar esta directriz con seguridad? Si heredo de List y resulta que necesito una API pública, ¿qué dificultades tendré?

¿Por qué importa? Una lista es una lista. ¿Qué podría cambiar? ¿Qué podría querer cambiar?

Y por último, si Microsoft no quería que yo heredara List, ¿por qué no entraron en la clase sealed?

¿Qué más se supone que debo usar?

Aparentemente, para las colecciones personalizadas, Microsoft ha proporcionado una Collectionclase que debería ampliarse en lugar de List. Pero esta clase es muy simple y no tiene muchas cosas útiles, comoAddRange , por ejemplo. La respuesta de jvitor83 proporciona una justificación de rendimiento para ese método en particular, pero ¿cómo es que un lento AddRangeno es mejor que no AddRange?

Heredar de Collectiones mucho más trabajo que heredar de List, y no veo ningún beneficio. Seguramente Microsoft no me diría que haga un trabajo adicional sin ningún motivo, por lo que no puedo evitar sentir que de alguna manera estoy malinterpretando algo, y heredar en Collectionrealidad no es la solución adecuada para mi problema.

He visto sugerencias como implementar IList. Simplemente no. Son docenas de líneas de código repetitivo que no me aportan nada.

Por último, algunos sugieren envolver el Listalgo:

class FootballTeam 
{ 
    public List<FootballPlayer> Players; 
}

Hay dos problemas con esto:

  1. Hace que mi código sea innecesariamente detallado. Ahora debo llamar en my_team.Players.Countlugar de solo my_team.Count. Afortunadamente, con C # puedo definir indexadores para hacer que la indexación sea transparente y reenviar todos los métodos internos List... ¡Pero eso es mucho código! ¿Qué obtengo por todo ese trabajo?

  2. Simplemente no tiene ningún sentido. Un equipo de fútbol no "tiene" una lista de jugadores. Que es la lista de jugadores. No dice "John McFootballer se ha unido a los jugadores de SomeTeam". Dices "John se ha unido a SomeTeam". No agrega una letra a "los caracteres de una cadena", agrega una letra a una cadena. No agrega un libro a los libros de una biblioteca, agrega un libro a una biblioteca.

Me doy cuenta de que se puede decir que lo que sucede "debajo del capó" es "agregar X a la lista interna de Y", pero esto parece una forma muy contraria a la intuición de pensar sobre el mundo.

Mi pregunta (resumida)

¿Cuál es la correcta C # forma de representar una estructura de datos, que, "lógicamente" (es decir, "a la mente humana") es sólo una listde las thingscon algunas campanas y silbidos?

¿Heredar de List<T>siempre es inaceptable? ¿Cuándo es aceptable? ¿Por qué por qué no? ¿Qué debe considerar un programador al decidir si heredar List<T>o no?

Answer

Aquí hay algunas buenas respuestas. Les añadiría los siguientes puntos.

What is the correct C# way of representing a data structure, which, "logically" (that is to say, "to the human mind") is just a list of things with a few bells and whistles?

Pida a diez personas que no sean programadores informáticos y que estén familiarizadas con la existencia del fútbol que llenen el espacio en blanco:

A football team is a particular kind of _____

¿ Alguien dijo "lista de jugadores de fútbol con algunas campanas y silbidos", o todos dijeron "equipo deportivo" o "club" u "organización"? Tu idea de que un equipo de fútbol es un tipo particular de lista de jugadores está en tu mente humana y solo en tu mente humana.

List<T>es un mecanismo . El equipo de fútbol es un objeto comercial , es decir, un objeto que representa algún concepto que pertenece al dominio comercial del programa. ¡No los mezcles! Un equipo de fútbol es una especie de equipo; que tiene una lista, una lista es una lista de jugadores . Una lista no es un tipo particular de lista de jugadores . Una lista es una lista de jugadores. Así que crea una propiedad llamada Rosterque sea a List<Player>. Y hágalo ReadOnlyList<Player>mientras lo hace, a menos que crea que todos los que conocen un equipo de fútbol pueden eliminar jugadores de la lista.

Is inheriting from List<T> always unacceptable?

¿Inaceptable para quién? ¿Me? No.

When is it acceptable?

Cuando está construyendo un mecanismo que amplía el List<T>mecanismo .

What must a programmer consider, when deciding whether to inherit from List<T> or not?

¿Estoy construyendo un mecanismo o un objeto comercial ?

But that's a lot of code! What do I get for all that work?

Pasó más tiempo escribiendo su pregunta de lo que le habría llevado escribir métodos de reenvío para los miembros relevantes de List<T>cincuenta veces. Claramente no le teme a la verbosidad, y estamos hablando de una cantidad muy pequeña de código aquí; esto es un trabajo de unos minutos.

ACTUALIZAR

Lo pensé un poco más y hay otra razón para no modelar un equipo de fútbol como una lista de jugadores. De hecho, podría ser una mala idea modelar un equipo de fútbol con una lista de jugadores también. El problema con un equipo que tiene una lista de jugadores es que lo que tienes es una instantánea del equipo en un momento determinado . No sé cuál es su caso comercial para esta clase, pero si tuviera una clase que representara a un equipo de fútbol, ​​me gustaría hacerle preguntas como "¿cuántos jugadores de los Seahawks se perdieron partidos debido a una lesión entre 2003 y 2013?" o "¿Qué jugador de Denver que jugó anteriormente para otro equipo tuvo el mayor aumento año tras año en yardas corridas?" o " ¿Los Piggers llegaron hasta el final este año? "

Es decir, me parece que un equipo de fútbol está bien modelado como una colección de hechos históricos , como cuándo un jugador fue reclutado, lesionado, retirado, etc. Obviamente, la lista de jugadores actual es un hecho importante que probablemente debería estar al frente y al frente. center, pero puede haber otras cosas interesantes que desee hacer con este objeto que requieran una perspectiva más histórica.

Vaya, tu publicación tiene una gran cantidad de preguntas y puntos. La mayor parte del razonamiento que obtiene de Microsoft es exactamente acertado. Empecemos con todo sobreList<T>

  • List<T> está altamente optimizado. Su uso principal es ser un miembro privado de un objeto.
  • Microsoft no cerraba porque a veces es posible que desee crear una clase que tiene un nombre más sencillo: class MyList<T, TX> : List<CustomObject<T, Something<TX>> { ... }. Ahora es tan fácil como hacerlo var list = new MyList<int, string>();.
  • CA1002: No exponga listas genéricas : Básicamente, incluso si planea usar esta aplicación como el único desarrollador, vale la pena desarrollarla con buenas prácticas de codificación, para que se le inculquen y se conviertan en una segunda naturaleza. Aún puede exponer la lista como IList<T>si necesita que algún consumidor tenga una lista indexada. Esto le permite cambiar la implementación dentro de una clase más adelante.
  • Microsoft lo hizo Collection<T>muy genérico porque es un concepto genérico ... el nombre lo dice todo; es solo una colección. Hay versiones más precisas, tales como SortedCollection<T>, ObservableCollection<T>, ReadOnlyCollection<T>, etc. cada uno de los cuales implementan IList<T>pero no List<T> .
  • Collection<T>permite que los miembros (es decir, Agregar, Eliminar, etc.) se anulen porque son virtuales. List<T>no.
  • La última parte de tu pregunta es acertada. Un equipo de fútbol es más que una lista de jugadores, por lo que debería ser una clase que contenga esa lista de jugadores. Piense en la composición frente a la herencia . Un equipo de fútbol tiene una lista de jugadores (una lista), no es una lista de jugadores.

Si estuviera escribiendo este código, la clase probablemente se vería así:

public class FootballTeam
{
    // Football team rosters are generally 53 total players.
    private readonly List<T> _roster = new List<T>(53);

    public IList<T> Roster
    {
        get { return _roster; }
    }

    // Yes. I used LINQ here. This is so I don't have to worry about
    // _roster.Length vs _roster.Count vs anything else.
    public int PlayerCount
    {
        get { return _roster.Count(); }
    }

    // Any additional members you want to expose/wrap.
}
class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal;
}

El código anterior significa: un grupo de chicos de la calle jugando al fútbol, ​​y resulta que tienen un nombre. Algo como:

Gente jugando futbol

De todos modos, este código (de mi respuesta)

public class FootballTeam
{
    // A team's name
    public string TeamName; 

    // Football team rosters are generally 53 total players.
    private readonly List<T> _roster = new List<T>(53);

    public IList<T> Roster
    {
        get { return _roster; }
    }

    public int PlayerCount
    {
        get { return _roster.Count(); }
    }

    // Any additional members you want to expose/wrap.
}

Medios: este es un equipo de fútbol que tiene gerencia, jugadores, administradores, etc. Algo como:

Imagen que muestra a los miembros (y otros atributos) del equipo Manchester United

Así es como se presenta tu lógica en imágenes ...

Este es un ejemplo clásico de composición frente a herencia .

En este caso específico:

¿Es el equipo una lista de jugadores con comportamiento añadido?

o

¿Es el equipo un objeto propio que contiene una lista de jugadores?

Al extender List, se está limitando de varias maneras:

  1. No puede restringir el acceso (por ejemplo, evitar que las personas cambien la lista). Obtiene todos los métodos de lista, ya sea que los necesite / desee todos o no.

  2. ¿Qué sucede si también quieres tener listas de otras cosas? Por ejemplo, los equipos tienen entrenadores, gerentes, aficionados, equipamiento, etc. Algunos de ellos bien podrían ser listas por derecho propio.

  3. Limitas tus opciones de herencia. Por ejemplo, es posible que desee crear un objeto Team genérico y luego tener BaseballTeam, FootballTeam, etc.que herede de eso. Para heredar de List, debe hacer la herencia de Team, pero eso significa que todos los diversos tipos de equipo se ven obligados a tener la misma implementación de esa lista.

Composición: incluido un objeto que proporciona el comportamiento que desea dentro de su objeto.

Herencia: su objeto se convierte en una instancia del objeto que tiene el comportamiento que desea.

Ambos tienen sus usos, pero este es un caso claro en el que se prefiere la composición.

Como todos han señalado, un equipo de jugadores no es una lista de jugadores. Este error lo cometen muchas personas en todas partes, tal vez en varios niveles de experiencia. A menudo, el problema es sutil y, en ocasiones, muy grave, como en este caso. Tales diseños son malos porque violan el principio de sustitución de Liskov . Internet tiene muchos artículos buenos que explican este concepto, por ejemplo, http://en.wikipedia.org/wiki/Liskov_substitution_principle

En resumen, hay dos reglas que deben conservarse en una relación padre / hijo entre clases:

  • un Niño no debe requerir ninguna característica menor que la que define completamente al Padre.
  • un Padre no debe requerir ninguna característica además de lo que define completamente al Niño.

En otras palabras, un padre es una definición necesaria de un niño, y un niño es una definición suficiente de un padre.

Aquí hay una manera de pensar en la solución de uno y aplicar el principio anterior que debería ayudar a evitar tal error. Uno debe probar su hipótesis verificando si todas las operaciones de una clase padre son válidas para la clase derivada tanto estructural como semánticamente.

  • ¿Es un equipo de fútbol una lista de jugadores de fútbol? (¿Todas las propiedades de una lista se aplican a un equipo con el mismo significado?)
    • ¿Es un equipo una colección de entidades homogéneas? Sí, el equipo es una colección de jugadores.
    • ¿El orden de inclusión de los jugadores es descriptivo del estado del equipo y el equipo se asegura de que la secuencia se mantenga a menos que se cambie explícitamente? No y no
    • ¿Se espera que los jugadores sean incluidos / eliminados en función de su posición secuencial en el equipo? No

Como ve, solo la primera característica de una lista es aplicable a un equipo. Por tanto, un equipo no es una lista. Una lista sería un detalle de implementación de cómo administra su equipo, por lo que solo debe usarse para almacenar los objetos del jugador y manipularse con métodos de la clase Equipo.

En este punto, me gustaría comentar que una clase de equipo, en mi opinión, ni siquiera debería implementarse usando una lista; debe implementarse usando una estructura de datos Set (HashSet, por ejemplo) en la mayoría de los casos.

¿Qué pasa si FootballTeamtiene un equipo de reserva junto con el equipo principal?

class FootballTeam
{
    List<FootballPlayer> Players { get; set; }
    List<FootballPlayer> ReservePlayers { get; set; }
}

¿Cómo modelarías eso?

class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal 
}

La relación claramente tiene una y no es una .

o RetiredPlayers?

class FootballTeam
{
    List<FootballPlayer> Players { get; set; }
    List<FootballPlayer> ReservePlayers { get; set; }
    List<FootballPlayer> RetiredPlayers { get; set; }
}

Como regla general, si alguna vez desea heredar de una colección, nombre la clase SomethingCollection.

¿Tu SomethingCollectionsemánticamente tiene sentido? Solo haga esto si su tipo es una colección de Something.

En el caso de FootballTeamque no suene bien. A Teames más que a Collection. A Teampuede tener entrenadores, entrenadores, etc., como han señalado las otras respuestas.

FootballCollectionsuena como una colección de balones de fútbol o tal vez una colección de parafernalia de fútbol. TeamCollection, una colección de equipos.

FootballPlayerCollectionsuena como una colección de jugadores que sería un nombre válido para una clase que hereda List<FootballPlayer>si realmente quisieras hacer eso.

Realmente List<FootballPlayer>es un tipo perfectamente bueno para tratar. Quizás IList<FootballPlayer>si lo devuelve de un método.

En resumen

Pregúntese

  1. Es X un Y? o tiene X un Y?

  2. ¿Los nombres de mis clases significan lo que son?

Diseño> Implementación

Los métodos y propiedades que expone es una decisión de diseño. La clase base de la que hereda es un detalle de implementación. Siento que vale la pena dar un paso atrás al primero.

Un objeto es una colección de datos y comportamiento.

Entonces tus primeras preguntas deberían ser:

  • ¿Qué datos comprende este objeto en el modelo que estoy creando?
  • ¿Qué comportamiento exhibe este objeto en ese modelo?
  • ¿Cómo podría cambiar esto en el futuro?

Tenga en cuenta que la herencia implica una relación "isa" (es a), mientras que la composición implica una relación "tiene a" (hasa). Elija el adecuado para su situación en su vista, teniendo en cuenta dónde pueden ir las cosas a medida que evoluciona su aplicación.

Considere pensar en interfaces antes de pensar en tipos concretos, ya que a algunas personas les resulta más fácil poner su cerebro en "modo de diseño" de esa manera.

Esto no es algo que todo el mundo haga conscientemente a este nivel en la codificación del día a día. Pero si está reflexionando sobre este tipo de tema, está pisando aguas del diseño. Ser consciente de ello puede ser liberador.

Considere los detalles del diseño

Echar un vistazo a List<T>, y IList<T>en MSDN o Visual Studio. Vea qué métodos y propiedades exponen. ¿Todos estos métodos se ven como algo que alguien querría hacer FootballTeamen su opinión?

¿Tiene footballTeam.Reverse()sentido para ti? ¿ footballTeam.ConvertAll<TOutput>()Parece algo que quieres?

Esta no es una pregunta capciosa; la respuesta podría ser genuinamente "sí". Si implementa / hereda List<Player>o IList<Player>, está atascado con ellos; si eso es ideal para su modelo, hágalo.

Si decide que sí, tiene sentido y desea que su objeto sea tratable como una colección / lista de jugadores (comportamiento) y, por lo tanto, desea implementarlo ICollection<Player>o IList<Player>, por supuesto, hacerlo. Teóricamente:

class FootballTeam : ... ICollection<Player>
{
    ...
}

Si desea que su objeto contenga una colección / lista de jugadores (datos) y, por lo tanto, desea que la colección o lista sea una propiedad o miembro, hágalo por todos los medios. Teóricamente:

class FootballTeam ...
{
    public ICollection<Player> Players { get { ... } }
}

Es posible que sienta que desea que las personas solo puedan enumerar el conjunto de jugadores, en lugar de contarlos, agregarlos o eliminarlos. IEnumerable<Player>es una opción perfectamente válida a considerar.

Es posible que sienta que ninguna de estas interfaces es útil en su modelo. Esto es menos probable ( IEnumerable<T>es útil en muchas situaciones) pero aún es posible.

Cualquiera que intente decirte que uno de estos es categórica y definitivamente equivocado en todos los casos está equivocado. Cualquiera que intente decirte que tiene razón categórica y definitivamente en todos los casos está equivocado.

Pasar a la implementación

Una vez que haya decidido los datos y el comportamiento, puede tomar una decisión sobre la implementación. Esto incluye las clases concretas de las que depende por herencia o composición.

Puede que este no sea un gran paso, y la gente a menudo combina diseño e implementación, ya que es muy posible repasarlo todo en su cabeza en uno o dos segundos y comenzar a escribir.

Un experimento mental

Un ejemplo artificial: como han mencionado otros, un equipo no siempre es "solo" una colección de jugadores. ¿Mantiene una colección de resultados de partidos para el equipo? ¿Es el equipo intercambiable con el club, en tu modelo? Si es así, y si su equipo es una colección de jugadores, quizás también sea una colección de personal y / o una colección de puntuaciones. Entonces terminas con:

class FootballTeam : ... ICollection<Player>, 
                         ICollection<StaffMember>,
                         ICollection<Score>
{
    ....
}

Independientemente del diseño, en este punto en C # no podrá implementar todos estos heredando de List<T>todos modos, ya que C # "solo" admite la herencia única. (Si ha probado este malarkey en C ++, puede considerarlo algo bueno). Es probable que la implementación de una colección a través de la herencia y otra a través de la composición se sienta sucia. Y las propiedades como se Countvuelven confusas para los usuarios a menos que implemente ILIst<Player>.County IList<StaffMember>.Countetc. explícitamente, y luego son dolorosas en lugar de confusas. Puedes ver a dónde va esto; La intuición mientras piensa en esta avenida bien puede decirle que se siente mal ir en esta dirección (y, con razón o sin ella, ¡sus colegas también podrían hacerlo si lo implementa de esta manera!)

La respuesta corta (demasiado tarde)

La guía sobre no heredar de clases de colección no es específica de C #, la encontrará en muchos lenguajes de programación. Se recibe sabiduría, no ley. Una razón es que, en la práctica, se considera que la composición a menudo gana sobre la herencia en términos de comprensibilidad, implementabilidad y mantenibilidad. Es más común con objetos del mundo real / dominio encontrar relaciones "hasa" útiles y consistentes que relaciones "isa" útiles y consistentes, a menos que estés en lo abstracto, especialmente a medida que pasa el tiempo y los datos precisos y el comportamiento de los objetos en el código. cambios. Esto no debería hacer que descarte siempre heredar de clases de colección; pero puede resultar sugerente.

En primer lugar, tiene que ver con la usabilidad. Si usa la herencia, la Teamclase expondrá el comportamiento (métodos) que están diseñados exclusivamente para la manipulación de objetos. Por ejemplo, los métodos AsReadOnly()o CopyTo(obj)no tienen sentido para el objeto de equipo. En lugar del AddRange(items)método, probablemente desee un AddPlayers(players)método más descriptivo .

Si desea utilizar LINQ, implementar una interfaz genérica como ICollection<T>o IEnumerable<T>tendría más sentido.

Como se mencionó, la composición es la forma correcta de hacerlo. Simplemente implemente una lista de jugadores como variable privada.

Déjame reescribir tu pregunta. para que pueda ver el tema desde una perspectiva diferente.

Cuando necesito representar a un equipo de fútbol, ​​entiendo que es básicamente un nombre. Me gusta: "The Eagles"

string team = new string();

Luego, más tarde, me di cuenta de que los equipos también tienen jugadores.

¿Por qué no puedo simplemente extender el tipo de cadena para que también contenga una lista de jugadores?

Su punto de entrada al problema es arbitrario. Trate de pensar qué tiene (propiedades) un equipo , no qué es .

Después de hacer eso, podría ver si comparte propiedades con otras clases. Y piensa en la herencia.

depende del contexto

Cuando consideras a tu equipo como una lista de jugadores, estás proyectando la "idea" de un equipo de fútbol americano en un aspecto: reduces el "equipo" a las personas que ves en el campo. Esta proyección solo es correcta en un contexto determinado. En un contexto diferente, esto podría estar completamente equivocado. Imagina que quieres convertirte en patrocinador del equipo. Entonces tienes que hablar con los gerentes del equipo. En este contexto, el equipo se proyecta a la lista de sus gerentes. Y estas dos listas generalmente no se superponen mucho. Otros contextos son los jugadores actuales frente a los ex jugadores, etc.

Semántica poco clara

Entonces, el problema de considerar a un equipo como una lista de sus jugadores es que su semántica depende del contexto y que no puede extenderse cuando el contexto cambia. Además, es difícil de expresar qué contexto está utilizando.

Las clases son extensibles

Cuando usa una clase con un solo miembro (por ejemplo IList activePlayers), puede usar el nombre del miembro (y además su comentario) para aclarar el contexto. Cuando hay contextos adicionales, simplemente agrega un miembro adicional.

Las clases son más complejas

En algunos casos, puede resultar excesivo crear una clase adicional. Cada definición de clase debe cargarse a través del cargador de clases y la máquina virtual la almacenará en caché. Esto le cuesta rendimiento y memoria en tiempo de ejecución. Cuando tienes un contexto muy específico, puede estar bien considerar un equipo de fútbol como una lista de jugadores. Pero en este caso, realmente debería usar a IList , no una clase derivada de él.

Conclusión / Consideraciones

Cuando tienes un contexto muy específico, está bien considerar un equipo como una lista de jugadores. Por ejemplo, dentro de un método, está completamente bien escribir:

IList<Player> footballTeam = ...

Al usar F #, incluso puede estar bien crear una abreviatura de tipo:

type FootballTeam = IList<Player>

Pero cuando el contexto es más amplio o incluso poco claro, no debe hacer esto. Este es especialmente el caso cuando crea una nueva clase cuyo contexto en el que se puede usar en el futuro no está claro. Una señal de advertencia es cuando comienzas a agregar atributos adicionales a tu clase (nombre del equipo, entrenador, etc.). Esta es una clara señal de que el contexto en el que se utilizará la clase no es fijo y cambiará en el futuro. En este caso, no puede considerar al equipo como una lista de jugadores, pero debe modelar la lista de los jugadores (actualmente activos, no lesionados, etc.) como un atributo del equipo.

Un equipo de fútbol no es una lista de jugadores de fútbol. ¡Un equipo de fútbol se compone de una lista de jugadores de fútbol!

Esto es lógicamente incorrecto:

class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal 
}

y esto es correcto:

class FootballTeam 
{ 
    public List<FootballPlayer> players
    public string TeamName; 
    public int RunningTotal 
}

Solo porque creo que las otras respuestas se desvían por la tangente de si un equipo de fútbol "es-un" List<FootballPlayer>o "tiene-un" List<FootballPlayer>, lo que realmente no responde a esta pregunta tal como está escrita.

El OP solicita principalmente una aclaración sobre las pautas para heredar de List<T>:

A guideline says that you shouldn't inherit from List<T>. Why not?

Porque List<T>no tiene métodos virtuales. Esto es un problema menor en su propio código, ya que generalmente puede cambiar la implementación con relativamente poco esfuerzo, pero puede ser un problema mucho mayor en una API pública.

What is a public API and why should I care?

Una API pública es una interfaz que expone a programadores externos. Piense en el código marco. Y recuerde que las pautas a las que se hace referencia son las " Pautas de diseño de .NET Framework " y no las " Pautas de diseño de aplicaciones .NET ". Hay una diferencia y, en términos generales, el diseño de API públicas es mucho más estricto.

If my current project does not and is not likely to ever have this public API, can I safely ignore this guideline? If I do inherit from List and it turns out I need a public API, what difficulties will I have?

Bastante, sí. Es posible que desee considerar la justificación detrás de esto para ver si se aplica a su situación de todos modos, pero si no está creando una API pública, entonces no debe preocuparse particularmente por las preocupaciones de la API como el control de versiones (de las cuales, esta es una subconjunto).

Si agrega una API pública en el futuro, deberá abstraer su API de su implementación (al no exponerla List<T>directamente) o violar las pautas con el posible dolor futuro que ello conlleva.

Why does it even matter? A list is a list. What could possibly change? What could I possibly want to change?

Depende del contexto, pero como lo estamos usando FootballTeamcomo ejemplo, imagina que no puedes agregar un FootballPlayersi eso haría que el equipo supere el tope salarial. Una posible forma de agregar eso sería algo como:

 class FootballTeam : List<FootballPlayer> {
     override void Add(FootballPlayer player) {
        if (this.Sum(p => p.Salary) + player.Salary > SALARY_CAP)) {
          throw new InvalidOperationException("Would exceed salary cap!");
        }
     }
 }

Ah ... pero no puedes override Addporque no lo es virtual(por motivos de rendimiento).

Si está en una aplicación (lo que, básicamente, significa que usted y todas las personas que llaman están compilados juntos), ahora puede cambiar a usar IList<T>y corregir cualquier error de compilación:

 class FootballTeam : IList<FootballPlayer> {
     private List<FootballPlayer> Players { get; set; }

     override void Add(FootballPlayer player) {
        if (this.Players.Sum(p => p.Salary) + player.Salary > SALARY_CAP)) {
          throw new InvalidOperationException("Would exceed salary cap!");
        }
     }
     /* boiler plate for rest of IList */
 }

pero, si se ha expuesto públicamente a un tercero, acaba de realizar un cambio importante que provocará errores de compilación y / o tiempo de ejecución.

TL; DR: las pautas son para API públicas. Para las API privadas, haga lo que quiera.

¿Permitir que la gente diga

myTeam.subList(3, 5);

¿Tiene algún sentido en absoluto? Si no es así, no debería ser una lista.

Aquí hay muchas respuestas excelentes, pero quiero mencionar algo que no vi mencionado: el diseño orientado a objetos se trata de empoderar a los objetos .

Desea encapsular todas sus reglas, trabajo adicional y detalles internos dentro de un objeto apropiado. De esta manera, otros objetos que interactúan con este no tienen que preocuparse por todo. De hecho, desea ir un paso más allá y evitar activamente que otros objetos pasen por alto estos componentes internos.

Cuando hereda List, todos los demás objetos pueden verlo como una Lista. Tienen acceso directo a los métodos para agregar y eliminar jugadores. Y habrás perdido el control; por ejemplo:

Suponga que quiere diferenciar cuando un jugador se va sabiendo si se retiró, dimitió o fue despedido. Podría implementar un RemovePlayermétodo que tome una enumeración de entrada adecuada. Sin embargo, al heredar de List, no podrá evitar el acceso directo a Remove, RemoveAlle incluso Clear. Como resultado, en realidad le ha quitado poder a su FootballTeamclase.


Reflexiones adicionales sobre la encapsulación ... Ha planteado la siguiente inquietud:

It makes my code needlessly verbose. I must now call my_team.Players.Count instead of just my_team.Count.

Tiene razón, eso sería innecesariamente detallado para todos los clientes para usar su equipo. Sin embargo, ese problema es muy pequeño en comparación con el hecho de que ha expuesto List Playersa todos y cada uno para que puedan jugar con su equipo sin su consentimiento.

Continúas diciendo:

It just plain doesn't make any sense. A football team doesn't "have" a list of players. It is the list of players. You don't say "John McFootballer has joined SomeTeam's players". You say "John has joined SomeTeam".

Te equivocas en lo que respecta al primer bit: elimina la palabra "lista" y, en realidad, es obvio que un equipo tiene jugadores.
Sin embargo, da en el clavo con el segundo. No quiere que los clientes llamen ateam.Players.Add(...). Quieres que te llamen ateam.AddPlayer(...). Y su implementación (posiblemente entre otras cosas) llamaría Players.Add(...)internamente.


Con suerte, puede ver lo importante que es la encapsulación para el objetivo de potenciar sus objetos. Desea permitir que cada clase haga bien su trabajo sin temor a la interferencia de otros objetos.

Depende del comportamiento de su objeto "equipo". Si se comporta como una colección, podría estar bien representarlo primero con una Lista simple. Entonces puede comenzar a notar que sigue duplicando el código que se repite en la lista; en este punto tienes la opción de crear un objeto FootballTeam que envuelve la lista de jugadores. La clase FootballTeam se convierte en el hogar de todo el código que se repite en la lista de jugadores.

It makes my code needlessly verbose. I must now call my_team.Players.Count instead of just my_team.Count. Thankfully, with C# I can define indexers to make indexing transparent, and forward all the methods of the internal List... But that's a lot of code! What do I get for all that work?

Encapsulamiento. Sus clientes no necesitan saber lo que sucede dentro de FootballTeam. Para todos sus clientes, podría implementarse buscando la lista de jugadores en una base de datos. No necesitan saberlo y esto mejora su diseño.

It just plain doesn't make any sense. A football team doesn't "have" a list of players. It is the list of players. You don't say "John McFootballer has joined SomeTeam's players". You say "John has joined SomeTeam". You don't add a letter to "a string's characters", you add a letter to a string. You don't add a book to a library's books, you add a book to a library.

Exactamente :) dirás footballTeam.Add (john), no footballTeam.List.Add (john). La lista interna no será visible.

What is the correct C# way of representing a data structure...

Recuerde, "Todos los modelos son incorrectos, pero algunos son útiles". - Caja de George EP

No existe una "forma correcta", solo una útil.

Elija uno que sea útil para usted y sus usuarios. Eso es todo. Desarrolle económicamente, no aplique demasiada ingeniería. Cuanto menos código escriba, menos código necesitará depurar. (lea las siguientes ediciones).

- Editado

Mi mejor respuesta sería ... depende. Heredar de una lista expondría a los clientes de esta clase a métodos que pueden no estar expuestos, principalmente porque FootballTeam parece una entidad comercial.

- Edición 2

Sinceramente, no recuerdo a qué me estaba refiriendo en el comentario de "no sobredimensionar". Si bien creo que la mentalidad de KISS es una buena guía, quiero enfatizar que heredar una clase empresarial de List crearía más problemas de los que resuelve, debido a una fuga de abstracción .

Por otro lado, creo que hay un número limitado de casos en los que simplemente heredar de List es útil. Como escribí en la edición anterior, depende. La respuesta a cada caso está fuertemente influenciada por el conocimiento, la experiencia y las preferencias personales.

Gracias a @kai por ayudarme a pensar con más precisión en la respuesta.

Esto me recuerda al "es un" versus "tiene un" compromiso. A veces es más fácil y tiene más sentido heredar directamente de una superclase. Otras veces tiene más sentido crear una clase independiente e incluir la clase de la que habría heredado como variable miembro. Aún puede acceder a la funcionalidad de la clase, pero no está vinculado a la interfaz ni a ninguna otra restricción que pueda provenir de heredar de la clase.

Que haces Como ocurre con muchas cosas ... depende del contexto. La guía que usaría es que para heredar de otra clase realmente debería haber una relación "es una". Entonces, si escribe una clase llamada BMW, podría heredar de Car porque un BMW realmente es un automóvil. Una clase Caballo puede heredar de la clase Mamífero porque un caballo en realidad es un mamífero en la vida real y cualquier funcionalidad Mamífero debería ser relevante para Caballo. Pero, ¿puedes decir que un equipo es una lista? Por lo que puedo decir, no parece que un Equipo realmente "sea" una Lista. Entonces, en este caso, tendría una Lista como variable miembro.

Lo que dicen las pautas es que la API pública no debe revelar la decisión de diseño interno de si está utilizando una lista, un conjunto, un diccionario, un árbol o lo que sea. Un "equipo" no es necesariamente una lista. Puede implementarlo como una lista, pero los usuarios de su API pública deben usar su clase según sea necesario. Esto le permite cambiar su decisión y utilizar una estructura de datos diferente sin afectar la interfaz pública.

Mi sucio secreto: no me importa lo que diga la gente, y lo hago. .NET Framework se extiende con "XxxxCollection" (UIElementCollection para el ejemplo de la parte superior de mi cabeza).

Entonces, ¿qué me detiene diciendo:

team.Players.ByName("Nicolas")

Cuando lo encuentro mejor que

team.ByName("Nicolas")

Además, mi PlayerCollection podría ser utilizada por otra clase, como "Club" sin duplicación de código.

club.Players.ByName("Nicolas")

Las mejores prácticas de ayer pueden no ser las de mañana. No hay ninguna razón detrás de la mayoría de las mejores prácticas, la mayoría son solo un amplio acuerdo entre la comunidad. En lugar de preguntarle a la comunidad si lo culpará a usted cuando lo haga, pregúntese, ¿qué es más legible y fácil de mantener?

team.Players.ByName("Nicolas") 

o

team.ByName("Nicolas")

En realidad. ¿Tienes alguna duda? Ahora, tal vez necesite jugar con otras limitaciones técnicas que le impiden usar List <T> en su caso de uso real. Pero no agregue una restricción que no debería existir. Si Microsoft no documentó el por qué, entonces seguramente es una "mejor práctica" que viene de la nada.

Cuando dicen que List<T>está "optimizado", creo que quieren decir que no tiene características como los métodos virtuales que son un poco más caros. Entonces, el problema es que una vez que expone List<T>en su API pública , pierde la capacidad de hacer cumplir las reglas comerciales o personalizar su funcionalidad más adelante. Pero si está utilizando esta clase heredada como interna dentro de su proyecto (en lugar de estar potencialmente expuesta a miles de sus clientes / socios / otros equipos como API), entonces puede estar bien si le ahorra tiempo y es la funcionalidad que desea duplicar. La ventaja de heredar de List<T>es que elimina una gran cantidad de código contenedor tonto que nunca se personalizará en un futuro previsible. Además, si desea que su clase tenga explícitamente exactamente la misma semántica queList<T> durante la vida de sus API, entonces también puede estar bien.

A menudo veo a mucha gente haciendo un montón de trabajo extra solo porque la regla FxCop lo dice o el blog de alguien dice que es una "mala" práctica. Muchas veces, esto convierte el código en una rareza de palooza de patrón de diseño. Al igual que con muchas pautas, trátelas como pautas que pueden tener excepciones.

Problemas con la serialización

Falta un aspecto. Las clases que heredan de List no se pueden serializar correctamente con XmlSerializer. En ese caso, se debe utilizar DataContractSerializer en su lugar, o se necesita una implementación de serialización propia.
public class DemoList : List<Demo>
{
    // using XmlSerializer this properties won't be seralized
    // There is no error, the data is simply not there.
    string AnyPropertyInDerivedFromList { get; set; }     
}

public class Demo
{
    // this properties will be seralized
    string AnyPropetyInDemo { get; set; }  
}

Lectura adicional: cuando una clase se hereda de List <>, XmlSerializer no serializa otros atributos

Utilice IList en su lugar

Personalmente, no heredaría de List, pero implementaría IList. Visual Studio hará el trabajo por usted y creará una iplementación de trabajo completa. Mire aquí: Cómo obtener una implementación funcional completa de IList

Si bien no tengo una comparación compleja como la mayoría de estas respuestas, me gustaría compartir mi método para manejar esta situación. Al extender IEnumerable<T>, puede permitir que su Teamclase admita extensiones de consulta Linq, sin exponer públicamente todos los métodos y propiedades de List<T>.

class Team : IEnumerable<Player>
{
    private readonly List<Player> playerList;

    public Team()
    {
        playerList = new List<Player>();
    }

    public Enumerator GetEnumerator()
    {
        return playerList.GetEnumerator();
    }

    ...
}

class Player
{
    ...
}

Solo quería agregar que Bertrand Meyer, el inventor de Eiffel y el diseño por contrato, habría Teamheredado List<Player>sin siquiera pestañear.

En su libro, Construcción de software orientado a objetos , analiza la implementación de un sistema GUI donde las ventanas rectangulares pueden tener ventanas secundarias. Simplemente ha Windowheredado de ambos Rectangley Tree<Window>reutilizar la implementación.

Sin embargo, C # no es Eiffel. Este último admite herencia múltiple y cambio de nombre de características . En C #, cuando crea una subclase, hereda tanto la interfaz como la implementación. Puede anular la implementación, pero las convenciones de llamada se copian directamente desde la superclase. En Eiffel, sin embargo, puede modificar los nombres de los métodos públicos, por lo que puede cambiar el nombre Addy Removea Hirey Fireen su Team. Si una instancia de Teames upcast back to List<Player>, la persona que llama usará Addy Removepara modificarla, pero sus métodos virtuales Hirey Fireserán llamados.

Si los usuarios de su clase necesitan todos los métodos y propiedades que tiene ** List, debe derivar su clase de ella. Si no los necesitan, adjunte la Lista y cree envoltorios para los métodos que los usuarios de su clase realmente necesitan.

Esta es una regla estricta, si escribe una API pública o cualquier otro código que será utilizado por muchas personas. Puede ignorar esta regla si tiene una aplicación pequeña y no más de 2 desarrolladores. Esto le ahorrará algo de tiempo.

Para aplicaciones pequeñas, también puede considerar elegir otro lenguaje menos estricto. Ruby, JavaScript: cualquier cosa que le permita escribir menos código.

Creo que no estoy de acuerdo con tu generalización. Un equipo no es solo una colección de jugadores. Un equipo tiene mucha más información al respecto: nombre, emblema, grupo de personal directivo / administrativo, grupo de entrenadores y luego grupo de jugadores. Así que correctamente, su clase FootballTeam debería tener 3 colecciones y no ser una colección en sí misma; si se trata de modelar correctamente el mundo real.

Podría considerar una clase PlayerCollection que, como Specialized StringCollection, ofrece otras facilidades, como validación y comprobaciones antes de que se agreguen o eliminen objetos de la tienda interna.

Tal vez, ¿la noción de un jugador mejor de PlayerCollection se adapta a su enfoque preferido?

public class PlayerCollection : Collection<Player> 
{ 
}

Y luego FootballTeam puede verse así:

public class FootballTeam 
{ 
    public string Name { get; set; }
    public string Location { get; set; }

    public ManagementCollection Management { get; protected set; } = new ManagementCollection();

    public CoachingCollection CoachingCrew { get; protected set; } = new CoachingCollection();

    public PlayerCollection Players { get; protected set; } = new PlayerCollection();
}

Preferir interfaces sobre clases

Las clases deben evitar derivar de clases y, en cambio, implementar las interfaces mínimas necesarias.

La herencia rompe la encapsulación

Derivar de clases rompe la encapsulación :

  • expone detalles internos sobre cómo se implementa su colección
  • declara una interfaz (conjunto de funciones y propiedades públicas) que puede no ser apropiada

Entre otras cosas, esto dificulta la refactorización de su código.

Las clases son un detalle de implementación

Las clases son un detalle de implementación que debe ocultarse de otras partes de su código. En resumen, a System.Listes una implementación específica de un tipo de datos abstracto, que puede o no ser apropiado ahora y en el futuro.

Conceptualmente, el hecho de que el System.Listtipo de datos se llame "lista" es una pista falsa. A System.List<T>es una colección ordenada mutable que admite operaciones O (1) amortizadas para agregar, insertar y eliminar elementos, y operaciones O (1) para recuperar el número de elementos o obtener y configurar elemento por índice.

Cuanto más pequeña es la interfaz, más flexible es el código

Al diseñar una estructura de datos, cuanto más simple es la interfaz, más flexible es el código. Solo mire lo poderoso que es LINQ para una demostración de esto.

Cómo elegir interfaces

Cuando piense en "lista", debería empezar por decirse a sí mismo: "Necesito representar una colección de jugadores de béisbol". Digamos que decides modelar esto con una clase. Lo que debe hacer primero es decidir cuál es la cantidad mínima de interfaces que esta clase necesitará exponer.

Algunas preguntas que pueden ayudar a guiar este proceso:

  • ¿Necesito tener el recuento? Si no, considere implementarIEnumerable<T>
  • ¿Esta colección va a cambiar después de que se haya inicializado? Si no considera IReadonlyList<T>.
  • ¿Es importante que pueda acceder a los elementos por índice? ConsiderarICollection<T>
  • ¿Es importante el orden en el que agrego elementos a la colección? ¿Quizás es un ISet<T>?
  • Si realmente desea estas cosas, continúe e impleméntelas IList<T>.

De esta manera, no vinculará otras partes del código con los detalles de implementación de su colección de jugadores de béisbol y podrá cambiar la forma en que se implementa siempre que respete la interfaz.

Al adoptar este enfoque, encontrará que el código se vuelve más fácil de leer, refactorizar y reutilizar.

Notas sobre cómo evitar el texto repetitivo

La implementación de interfaces en un IDE moderno debería ser fácil. Haga clic derecho y elija "Implementar interfaz". Luego, reenvíe todas las implementaciones a una clase miembro si es necesario.

Dicho esto, si descubre que está escribiendo mucho texto repetitivo, es potencialmente porque está exponiendo más funciones de las que debería. Es la misma razón por la que no deberías heredar de una clase.

También puede diseñar interfaces más pequeñas que tengan sentido para su aplicación, y tal vez solo un par de funciones de extensión de ayuda para asignar esas interfaces a cualquier otra que necesite. Este es el enfoque que tomé en mi propia IArrayinterfaz para la biblioteca LinqArray .

When is it acceptable?

Para citar a Eric Lippert:

When you're building a mechanism that extends the List<T> mechanism.

Por ejemplo, estás cansado de la ausencia del AddRangemétodo en IList<T>:

public interface IMoreConvenientListInterface<T> : IList<T>
{
    void AddRange(IEnumerable<T> collection);
}

public class MoreConvenientList<T> : List<T>, IMoreConvenientListInterface<T> { }