Pregunta ¿Por qué no heredar de List ?


Cuando planifico mis programas, a menudo empiezo con una cadena de pensamiento como esta:

Un equipo de fútbol es solo una lista de jugadores de fútbol. Por lo tanto, debería representarlo con:

var football_team = new List<FootballPlayer>();

El orden de esta lista representa el orden en que se enumeran los jugadores en la lista.

Pero luego me doy cuenta de que los equipos también tienen otras propiedades, además de la mera lista de jugadores, que deben registrarse. Por ejemplo, el total acumulado de puntajes esta temporada, el presupuesto actual, los colores uniformes, una string representando el nombre del equipo, etc.

Entonces pienso:

De acuerdo, un equipo de fútbol es como una lista de jugadores, pero, además, tiene un nombre (una string) y un total acumulado de puntajes (un int) .NET no proporciona una clase para almacenar equipos de fútbol, ​​por lo que haré mi propia clase. La estructura existente más similar y relevante es List<FootballPlayer>, entonces heredaré de ello:

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

Pero resulta que una guía dice que no debe heredar de List<T>. Estoy completamente confundido por esta guía en dos aspectos.

Por qué no?

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

Otra razón que he visto es que List es provisto por Microsoft, y no tengo control sobre él, por lo No puedo cambiarlo más tarde, después de exponer una "API pública". Pero me cuesta entender esto. ¿Qué es una API pública y por qué debería importarme? Si mi proyecto actual no tiene esta API pública y es probable que nunca la tenga, ¿puedo ignorar esta guía de forma segura? Si heredo de List  y resulta que necesito una API pública, ¿qué dificultades tendré?

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

Y, por último, si Microsoft no quería que heredara de List¿Por qué no hicieron la clase? sealed?

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

Aparentemente, para colecciones personalizadas, Microsoft ha proporcionado un Collection clase que debe extenderse en lugar de List. Pero esta clase es muy simple, y no tiene muchas cosas útiles, como AddRange, por ejemplo. La respuesta de jvitor83 proporciona una lógica de rendimiento para ese método en particular, pero ¿cómo es un lento AddRange no mejor que no AddRange?

Heredar de Collection es mucho más trabajo que heredar de Listy no veo ningún beneficio Seguramente Microsoft no me dirá que haga un trabajo extra sin ninguna razón, así que no puedo evitar sentir que de alguna manera estoy malinterpretando algo y heredando Collection en realidad no es la solución correcta para mi problema.

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

Por último, algunos sugieren envolver el List en algo:

class FootballTeam 
{ 
    public List<FootballPlayer> Players; 
}

Hay dos problemas con esto:

  1. Hace que mi código sea innecesariamente detallado. Ahora debo llamar my_team.Players.Count en lugar de simplemente my_team.Count. Afortunadamente, con C # puedo definir indexadores para que la indexación sea transparente, y reenviar todos los métodos de la interna 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. Eso es la lista de jugadores. Usted no dice "John McFootballer se ha unido a los jugadores de SomeTeam". Usted dice "John se unió 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 lo que sucede "debajo del capó" se puede decir que es "agregar X a la lista interna de Y", pero esto parece una manera muy contraintuitiva de pensar sobre el mundo.

Mi pregunta (resumida)

¿Cuál es la forma correcta de C # de representar una estructura de datos, que, "lógicamente" (es decir, "a la mente humana") es solo una list de things con algunas campanas y silbatos?

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


975
2018-02-11 03:01


origen


Respuestas:


Hay algunas buenas respuestas aquí. Les agregaría los siguientes puntos.

¿Cuál es la forma correcta de C # de representar una estructura de datos, que "lógicamente" (es decir, "a la mente humana") es solo una lista de cosas con algunas sonrisas y silbidos?

Pídale a cualquier persona que no sea programadora de computadoras que esté familiarizada con la existencia del fútbol que complete el espacio en blanco:

A football team is a particular kind of _____

Hizo nadie diga "lista de jugadores de fútbol con algunas campanas y silbatos", 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 en tu mente humana sola.

List<T> es un mecanismo. El equipo de fútbol es un objeto comercial - es decir, un objeto que representa algún concepto que está en el dominio de negocio Del programa. ¡No mezcles esos! Un equipo de fútbol Es un tipo de equipo; eso tiene un lista, una lista es una lista de jugadores. Una lista no es una tipo particular de lista de jugadores. Una lista es una lista de jugadores. Entonces haga una propiedad llamada Roster eso es un List<Player>. Y hazlo ReadOnlyList<Player> mientras estás en ello, a menos que creas que todos los que saben sobre un equipo de fútbol eliminan jugadores de la lista.

Está heredando de List<T> siempre inaceptable?

Inaceptable para quien? ¿Yo? No.

¿Cuándo es aceptable?

Cuando estás construyendo un mecanismo que extiende el List<T> mecanismo.

¿Qué debe considerar un programador al decidir si hereda de List<T> ¿o no?

¿Estoy construyendo un mecanismo o una objeto comercial?

¡Pero eso es mucho código! ¿Qué obtengo por todo ese trabajo?

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

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 como teniendo una lista de jugadores también. El problema con un equipo como / tener una lista de jugadores es que lo que tienes es un instantánea del equipo en un momento en el tiempo. No sé cuál es su caso de negocios para esta clase, pero si tuviera una clase que representara a un equipo de fútbol, ​​me gustaría formularle preguntas como "¿cuántos jugadores de los Seahawks se perdieron los juegos debido a una lesión entre 2003 y 2013?" o "¿Qué jugador de Denver que jugó anteriormente para otro equipo tuvo el mayor incremento anual en yardas?" o "¿Los Piggers fueron todo el camino este año?"

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


1137
2018-02-11 05:43



Por último, algunos sugieren envolver la Lista en algo:

Esa es la forma correcta. "Innecesariamente prolijo" es una mala forma de ver esto. Tiene un significado explícito cuando escribes my_team.Players.Count. Quieres contar los jugadores.

my_team.Count

..no significa nada. ¿Cuenta qué?

Un equipo no es una lista: consiste en más que solo una lista de jugadores. Un equipo posee jugadores, por lo que los jugadores deben ser parte de él (un miembro).

Si eres De Verdad preocupado porque es excesivamente detallado, siempre puede exponer las propiedades del equipo:

public int PlayerCount {
    get {
        return Players.Count;
    }
}

..que se convierte en:

my_team.PlayerCount

Esto tiene un significado ahora y se adhiere a La ley de Demeter.

También debería considerar adherirse a la Principio de reutilización compuesta. Al heredar de List<T>, dices que un equipo es un lista de jugadores y exponer métodos innecesarios. Esto es incorrecto: como dijiste, un equipo es más que una lista de jugadores: tiene un nombre, gerentes, miembros de la junta, entrenadores, personal médico, topes salariales, etc. Al hacer que tu clase de equipo contenga una lista de jugadores, Está diciendo "Un equipo tiene un lista de jugadores ", pero también puede tener otras cosas.


232
2018-02-11 03:05



Vaya, tu publicación tiene una gran cantidad de preguntas y puntos. La mayor parte del razonamiento que obtienes de Microsoft es exactamente correcto. Comencemos con todo sobre List<T>

  • List<T>  es altamente optimizado Su uso principal es para ser utilizado como un miembro privado de un objeto.
  • Microsoft no lo selló porque a veces es posible que desee crear una clase que tenga un nombre más amigable: class MyList<T, TX> : List<CustomObject<T, Something<TX>> { ... }. Ahora es tan fácil como hacer var list = new MyList<int, string>();.
  • CA1002: No exponga listas genéricas: Básicamente, incluso si planea usar esta aplicación como único desarrollador, vale la pena desarrollarla con buenas prácticas de codificación, para que se inculquen en usted y en la naturaleza. Aún se le permite exponer la lista como un IList<T> si necesita que algún consumidor tenga una lista indexada. Esto te permite cambiar la implementación dentro de una clase más adelante.
  • Microsoft 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, como SortedCollection<T>, ObservableCollection<T>, ReadOnlyCollection<T>, etc. cada uno de los cuales implementa IList<T> pero no  List<T>.
  • Collection<T> permite anular los miembros (es decir, Agregar, Eliminar, etc.) porque son virtuales. List<T> no.
  • La última parte de tu pregunta es perfecta. Un equipo de fútbol es más que solo una lista de jugadores, por lo que debe ser una clase que contenga esa lista de jugadores. Pensar Composición vs 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.
}

110
2018-02-11 03:26



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 tienen un nombre. Algo como:

People playing football

De todos modos, este código (de la respuesta de m-y)

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

    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 administración, jugadores, administradores, etc. Algo como:

Image showing members (and other attributes) of the Manchester United team

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


104
2018-02-11 15:45



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

En este caso específico:

Es el equipo una lista de jugadores con comportamiento adicional

o

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

Al extender List, te estás limitando de varias maneras:

  1. No puede restringir el acceso (por ejemplo, detener a las personas que cambian la lista). Obtiene todos los métodos de Lista, ya sea que los necesite o quiera todos o no.

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

  3. Usted limita sus opciones de herencia. Por ejemplo, puede querer crear un objeto de equipo genérico, y luego tener BaseballTeam, FootballTeam, etc. que hereden de eso. Para heredar de List, debe hacer la herencia del equipo, pero eso significa que todos los distintos tipos de equipo están obligados a tener la misma implementación de esa lista.

Composición: incluye 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 la composición es preferible.


81
2018-02-11 10:58



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, ocasionalmente, muy asqueroso, como en este caso. Tales diseños son malos porque estos violan el Principio de sustitución de Liskov. Internet tiene muchos buenos artículos que explican este concepto, por ejemplo, http://en.wikipedia.org/wiki/Liskov_substitution_principle 


43
2018-02-11 16:56



¿Qué pasa si el FootballTeam tiene un equipo de reservas 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 es claramente tiene un y no es un.

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 SomethingCollection semánticamente tiene sentido? Solo haz esto si tu tipo es un colección de Something.

En el caso de FootballTeam no suena bien UN Team es más que un Collection. UN Team puede tener entrenadores, entrenadores, etc. como las otras respuestas han señalado.

FootballCollection suena como una colección de balones de fútbol o quizás una colección de parafernalia de fútbol. TeamCollection, una colección de equipos.

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

De Verdad List<FootballPlayer> es un tipo perfectamente bueno para tratar. Tal vez 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 mi clase significan lo que son?


34
2018-02-12 11:41



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

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

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


27
2018-02-11 03:25