[c#] ¿Por qué no heredar de List <T>?



Answers

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 realmente te preocupa que sea excesivamente 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 debería considerar adherirse al principio de reutilización compuesta . Al heredar de List<T> , estás diciendo que un equipo es una lista de jugadores y expone los 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 una lista de jugadores", pero también puede tener otras cosas.

Question

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 representa 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> , por lo que heredaré de ella:

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?

Al parecer, List está optimizado de alguna manera 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 Microsoft proporciona List y no tengo control sobre él, por lo que 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 sealed la clase?

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

Aparentemente, para las colecciones personalizadas, Microsoft ha proporcionado una clase 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 lógica de rendimiento para ese método en particular, pero ¿cómo es un AddRange lento no mejor que no 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, así que no puedo evitar sentir que de alguna manera estoy malinterpretando algo, y heredar Collection 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 la 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 lugar de solo my_team.Count . Afortunadamente, con C # puedo definir indexadores para 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. 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 sonrisas 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 hereda de List<T> o no?




If your class users need all the methods and properties** List has, you should derive your class from it. If they don't need them, enclose the List and make wrappers for methods your class users actually need.

This is a strict rule, if you write a public API , or any other code that will be used by many people. You may ignore this rule if you have a tiny app and no more than 2 developers. This will save you some time.

For tiny apps, you may also consider choosing another, less strict language. Ruby, JavaScript - anything that allows you to write less code.




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

Remeber, "All models are wrong, but some are useful." - George EP Box

There is no a "correct way", only a useful one.

Choose one that is useful to you and/your users. Eso es. Develop economically, don't over-engineer. The less code you write, the less code you will need to debug. (read the following editions).

-- Edited

My best answer would be... it depends. Inheriting from a List would expose the clients of this class to methods that may be should not be exposed, primarily because FootballTeam looks like a business entity.

-- Edition 2

I sincerely don't remember to what I was referring on the “don't over-engineer” comment. While I believe the KISS mindset is a good guide, I want to emphasize that inheriting a business class from List would create more problems than it resolves, due abstraction leakage .

On the other hand, I believe there are a limited number of cases where simply to inherit from List is useful. As I wrote in the previous edition, it depends. The answer to each case is heavily influenced by both knowledge, experience and personal preferences.

Thanks to @kai for helping me to think more precisely about the answer.




What the guidelines say is that the public API should not reveal the internal design decision of whether you are using a list, a set, a dictionary, a tree or whatever. A "team" is not necessarily a list. You may implement it as a list but users of your public API should use you class on a need to know basis. This allows you to change your decision and use a different data structure without affecting the public interface.




Let me rewrite your question. so you might see the subject from a different perspective.

When I need to represent a football team, I understand that it is basically a name. Like: "The Eagles"

string team = new string();

Then later I realized teams also have players.

Why can't I just extend the string type so that it also holds a list of players?

Your point of entry into the problem is arbitrary. Try to think what does a team have (properties), not what it is .

After you do that, you could see if it shares properties with other classes. And think about inheritance.




This reminds me of the "Is a" versus "has a" tradeoff. Sometimes it is easier and makesmore sense to inherit directly from a super class. Other times it makes more sense to create a standalone class and include the class you would have inherited from as a member variable. You can still access the functionality of the class but are not bound to the interface or any other constraints that might come from inheriting from the class.

Which do you do? As with a lot of things...it depends on the context. The guide I would use is that in order to inherit from another class there truly should be an "is a" relationship. So if you a writing a class called BMW, it could inherit from Car because a BMW truly is a car. A Horse class can inherit from the Mammal class because a horse actually is a mammal in real life and any Mammal functionality should be relevant to Horse. But can you say that a team is a list? From what I can tell, it does not seem like a Team really "is a" List. So in this case, I would have a List as a member variable.




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

En resumen, hay dos reglas que se deben preservar en una relación Padre / Hijo entre clases:

  • un niño no debería requerir ninguna característica menor a la que define completamente al padre.
  • un padre no debería 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 la solución de uno y aplicar el principio anterior que debería ayudar a evitar ese error. Se deben probar las hipótesis verificando si todas las operaciones de una clase padre son válidas para la clase derivada estructural y 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 sentido)
    • ¿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 conserve a menos que se modifique explícitamente? No y no
    • ¿Se espera que los jugadores sean incluidos / descartados en función de su posición secuencial en el equipo? No

Como puede ver, solo la primera característica de una lista es aplicable a un equipo. Por lo tanto, un equipo no es una lista. Una lista sería un detalle de implementación de cómo administras tu equipo, por lo que solo se debe usar para almacenar los objetos del jugador y se debe manipular con métodos de clase Team.

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




En primer lugar, tiene que ver con la usabilidad. Si usa herencia, la clase Team expondrá el comportamiento (métodos) que están 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) , es probable que desee un método AddPlayers(players) 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. Solo implemente una lista de jugadores como una variable privada.




While I don't have a complex comparison as most of these answers do, I would like to share my method for handling this situation. By extending IEnumerable<T> , you can allow your Team class to support Linq query extensions, without publicly exposing all the methods and properties of 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
{
    ...
}



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:

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

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




A football team is not a list of football players. A football team is composed of a list of football players!

This is logically wrong:

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

and this is correct:

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



It depends on the behaviour of your "team" object. If it behaves just like a collection, it might be OK to represent it first with a plain List. Then you might start to notice that you keep duplicating code that iterates on the list; at this point you have the option of creating a FootballTeam object that wraps the list of players. The FootballTeam class becomes the home for all the code that iterates on the list of players.

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?

Encapsulation. Your clients need not know what goes on inside of FootballTeam. For all your clients know, it might be implemented by looking the list of players up in a database. They don't need to know, and this improves your design.

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.

Exactly :) you will say footballTeam.Add(john), not footballTeam.List.Add(john). The internal list will not be visible.




Links