c# herencia en - ¿Por qué no heredar de la lista <T>?





13 Answers

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

Esa es la forma correcta. "Sin palabras" es una mala manera 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 que?

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

Si realmente te preocupa que sea demasiado detallado, siempre puedes 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 debe considerar adherirse al principio de reutilización del material compuesto . Al heredar de la List<T> , estás diciendo que un equipo es una lista de jugadores y expone métodos innecesarios de ella. Esto es incorrecto: como usted dijo, 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 su clase de equipo contenga una lista de jugadores, usted estamos diciendo que "un equipo tiene una lista de jugadores", pero también puede tener otras cosas.

programación es el

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

Un equipo de fútbol es sólo 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 de esta temporada, el presupuesto actual, los colores uniformes, una string representa el nombre del equipo, etc.

Así que 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, ​​así que haré mi propia clase. La estructura existente más similar y relevante es la List<FootballPlayer> , por lo que heredaré de ella:

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

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

Por qué no?

Al parecer, la List está optimizada de alguna manera para el rendimiento . ¿Cómo es eso? ¿Qué problemas de rendimiento causaré si extiendo la List ? ¿Qué se romperá exactamente?

Otra razón que he visto es que Microsoft proporciona la List y no tengo control sobre ella, por lo que no puedo cambiarla 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 lo tiene y es probable que nunca tenga esta API pública, ¿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é importa? Una lista es una lista. ¿Qué podría cambiar? ¿Qué podría querer cambiar?

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

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

Al parecer, para colecciones personalizadas, Microsoft ha proporcionado una clase de Collection que debería 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 justificación de rendimiento para ese método en particular, pero ¿cómo un AddRange lento no es mejor que ningún AddRange ?

Heredar de Collection es mucho más trabajo que heredar de List , y no veo ningún beneficio. Seguramente Microsoft no me dirá que haga un trabajo extra sin ninguna razón, por lo que no puedo evitar sentirme como si de alguna manera estoy malinterpretando algo, y heredar Collection es realmente la solución adecuada 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 la List en algo:

class FootballTeam 
{ 
    public List<FootballPlayer> Players; 
}

Hay dos problemas con esto:

  1. Hace mi código innecesariamente detallado. Ahora debo llamar a my_team.Players.Count lugar de a my_team.Count . Afortunadamente, con C # puedo definir los indexadores para hacer que la indexación sea transparente y reenviar todos los métodos de la List interna ... ¡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. Es la lista de jugadores. No dices "John McFootballer se ha unido a los jugadores de SomeTeam". Usted dice "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 lo que sucede "bajo el capó" puede decirse que es "agregar X a la lista interna de Y", pero esto parece ser una forma muy poco intuitiva de pensar sobre el mundo.

Mi pregunta (resumida)

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

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




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

El código anterior significa: un grupo de muchachos de la calle que juegan al fútbol y que tienen un nombre. Algo como:

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

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 directivos, jugadores, administradores, etc. Algo así como:

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




Como todos han señalado, un equipo de jugadores no es una lista de jugadores. Muchas personas en todo el mundo cometen este error, tal vez con 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 las clases:

  • un niño no debe requerir ninguna característica menor a 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 hijo, y un hijo es una definición suficiente de un padre.

Aquí hay una manera de pensar en la solución y aplicar el principio anterior que debería ayudar a evitar tal error. Uno debe probar la hipótesis de uno al verificar 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 en 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 garantiza que la secuencia se conserve 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. De ahí que un equipo no sea una lista. Una lista sería un detalle de la implementación de cómo administras tu equipo, por lo que solo debe usarse para almacenar los objetos del jugador y manipularse con métodos de clase de equipo.

En este punto, me gustaría señalar que una clase de Equipo no debería, en mi opinión, implementarse utilizando una Lista; debe implementarse utilizando una estructura de datos de conjuntos (HashSet, por ejemplo) en la mayoría de los casos.




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

Si desea utilizar LINQ, la implementación de 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. Solo implementa una lista de jugadores como una variable privada.




Un equipo de fútbol no es una lista de jugadores de fútbol. Un equipo de fútbol está compuesto por 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 
}



Déjame reescribir tu pregunta. así que puedes ver el tema desde una perspectiva diferente.

Cuando necesito representar a un equipo de fútbol, ​​entiendo que es básicamente un nombre. Como: "Las águilas"

string team = new string();

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

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

Su punto de entrada en el problema es arbitrario. Trate de pensar lo que lo hace un equipo tiene (propiedades), no lo que es .

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




Permite que la gente diga

myTeam.subList(3, 5);

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




Aquí hay muchas respuestas excelentes, pero quiero referirme a algo que no vi mencionado: el diseño orientado a objetos se trata de potenciar 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 ello. De hecho, usted quiere ir un paso más allá y evitar activamente que otros objetos pasen por alto estas partes internas.

Cuando hereda de 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 tu control; por ejemplo:

Supongamos que quieres diferenciar cuando un jugador se va sabiendo si se retiraron, renunciaron o fueron despedidos. Podría implementar un RemovePlayermétodo que tome una enumeración de entrada apropiada. Sin embargo, al heredar de List, no podrá impedir el acceso directo a Remove, RemoveAlle incluso Clear. Como resultado, has desempoderado a tu FootballTeamclase.

Pensamientos adicionales sobre la encapsulación ... Usted planteó la siguiente preocupación:

Hace mi código innecesariamente detallado. Ahora debo llamar a my_team.Players.Count en lugar de a my_team.Count.

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

Usted va a decir:

Simplemente no tiene ningún sentido. Un equipo de fútbol no "tiene" una lista de jugadores. Es la lista de jugadores. No dices "John McFootballer se ha unido a los jugadores de SomeTeam". Usted dice "John se ha unido a SomeTeam".

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

Esperemos que pueda ver cuán importante es la encapsulación para el objetivo de potenciar sus objetos. Desea permitir que cada clase haga su trabajo bien sin temor a interferencias de otros objetos.




Esto me recuerda a "Is a" versus "tiene a" trade-off. A veces es más fácil y tiene más sentido heredar directamente de una súper clase. Otras veces tiene más sentido crear una clase independiente e incluir la clase que habría heredado como una 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 derivarse de la herencia de la clase.

Que haces Como 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". Por lo tanto, si escribe una clase llamada BMW, podría heredarse de un automóvil porque un BMW realmente es un automóvil. Una clase de Caballo puede heredar de la clase de Mamíferos porque un caballo en realidad es un mamífero en la vida real y cualquier funcionalidad de Mamíferos debe ser relevante para Caballos. ¿Pero puedes decir que un equipo es una lista? Por lo que puedo decir, no parece realmente un Equipo "es 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 usar una estructura de datos diferente sin afectar la interfaz pública.




Aunque 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ían Teamheredado de List<Player>tanto como de un pestillo.

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 realiza 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, para que pueda cambiar el nombre Addy Removepara Hirey Fireen su Team. Si una instancia de Teames upcast de nuevo a List<Player>la persona que llama se utilice Addy Removepara modificarlo, pero sus métodos virtuales Hirey Fireserá llamado.




Prefiere Interfaces sobre Clases

Las clases deben evitar derivarse de clases y, en su lugar, implementar las interfaces mínimas necesarias.

Herencia rompe la encapsulación

Derivando de las clases se 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 hace que sea más difícil refactorizar 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, System.Listes una implementación específica de un tipo de datos abstractos, 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 un poco engañoso. A System.List<T>es una colección ordenada mutable que admite operaciones amortizadas O (1) para agregar, insertar y eliminar elementos, y operaciones O (1) para recuperar la cantidad de elementos o obtener y configurar elementos por índice.

Cuanto más pequeña sea la interfaz, más flexible será el código.

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

Cómo elegir interfaces

Cuando pienses en "lista", debes comenzar diciéndote a ti mismo: "Necesito representar una colección de jugadores de béisbol". Así que digamos que decides modelar esto con una clase. Lo primero que debe hacer es decidir qué cantidad mínima de interfaces tendrá que exponer esta clase.

Algunas preguntas que pueden ayudar a guiar este proceso:

  • ¿Necesito tener el conde? Si no considera implementarIEnumerable<T>
  • ¿Esta colección va a cambiar después de que se haya inicializado? Si no lo consideramos IReadonlyList<T>.
  • ¿Es importante que pueda acceder a los artículos por índice? ConsiderarICollection<T>
  • ¿Es importante el orden en el que agrego artículos a la colección? Tal vez es un ISet<T>?
  • Si realmente quieres esto, entonces adelante e implementa IList<T>.

De esta manera, no unirá otras partes del código a los detalles de implementación de su colección de jugadores de béisbol y será libre de 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 la caldera

Implementar 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 un montón de repetitivo, es potencialmente porque está exponiendo más funciones de las que debería. Es la misma razón por la que no debe 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 auxiliares para asignar esas interfaces a las que necesite. Este es el enfoque que tomé en mi propia IArrayinterfaz para la biblioteca LinqArray .




Related