c# parameter Por que não herdar de List<T>?




t<> c# (21)

Ao planejar meus programas, muitas vezes começo com uma corrente de pensamento como esta:

Um time de futebol é apenas uma lista de jogadores de futebol. Portanto, eu deveria representá-lo com:

var football_team = new List<FootballPlayer>();

A ordenação desta lista representa a ordem em que os jogadores são listados na lista.

Mas percebo depois que as equipes também têm outras propriedades, além da simples lista de jogadores, que devem ser registradas. Por exemplo, o total de pontuações nesta temporada, o orçamento atual, as cores uniformes, uma string representando o nome da equipe, etc.

Então eu penso:

Ok, um time de futebol é como uma lista de jogadores, mas, além disso, tem um nome (uma string ) e um total de pontuações (um int ). O .NET não fornece uma classe para armazenar times de futebol, então eu farei minha própria aula. A estrutura existente mais semelhante e relevante é List<FootballPlayer> , então eu herdarei dela:

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

Mas acontece que uma diretriz diz que você não deve herdar de List<T> . Estou completamente confuso por esta diretriz em dois aspectos.

Por que não?

Aparentemente List é de alguma forma otimizado para desempenho . Como assim? Quais problemas de desempenho causarei se eu estender a List ? O que exatamente vai quebrar?

Outra razão que eu vi é que a List é fornecida pela Microsoft, e eu não tenho controle sobre ela, então não posso alterá-la mais tarde, depois de expor uma "API pública" . Mas eu me esforço para entender isso. O que é uma API pública e por que devo me importar? Se o meu projeto atual não tem e provavelmente nunca terá essa API pública, posso ignorar com segurança essa diretriz? Se eu herdar de List e acabar precisando de uma API pública, que dificuldades terei?

Por que isso importa? Uma lista é uma lista. O que poderia mudar? O que eu poderia querer mudar?

E por último, se a Microsoft não quisesse que eu herda da List , por que eles não sealed a classe?

O que mais devo usar?

Aparentemente, para coleções personalizadas, a Microsoft forneceu uma classe Collection que deve ser estendida em vez de List . Mas essa classe é muito simples e não tem muitas coisas úteis, como AddRange , por exemplo. A resposta de jvitor83 fornece uma justificativa de desempenho para esse método específico, mas como um AddRange lento não é melhor que nenhum AddRange ?

Herdar da Collection é muito mais trabalho do que herdar da List , e não vejo nenhum benefício. Certamente a Microsoft não me disse para fazer um trabalho extra sem motivo, então não posso deixar de sentir que estou de alguma forma entendendo mal alguma coisa, e herdar o Collection não é a solução certa para o meu problema.

Eu vi sugestões como implementar o IList . Apenas não. São dezenas de linhas de código clichê que não me dão nada.

Por último, alguns sugerem envolver a List em algo:

class FootballTeam 
{ 
    public List<FootballPlayer> Players; 
}

Existem dois problemas com isso:

  1. Isso torna meu código desnecessariamente detalhado. Agora devo chamar my_team.Players.Count vez de apenas my_team.Count . Felizmente, com o C #, posso definir indexadores para tornar a indexação transparente e encaminhar todos os métodos da List interna ... Mas isso é muito código! O que eu ganho com todo esse trabalho?

  2. Simplesmente não faz nenhum sentido. Um time de futebol não "tem" uma lista de jogadores. É a lista de jogadores. Você não diz "John McFootballer se juntou aos jogadores da SomeTeam". Você diz "John se juntou a SomeTeam". Você não adiciona uma letra a "caracteres de uma string", você adiciona uma letra a uma string. Você não adiciona um livro aos livros de uma biblioteca e adiciona um livro a uma biblioteca.

Eu percebo que o que acontece "sob o capô" pode ser dito como "adicionando X à lista interna de Y", mas isso parece ser uma maneira muito contra-intuitiva de pensar sobre o mundo.

Minha pergunta (resumida)

Qual é o modo C # correto de representar uma estrutura de dados, que, "logicamente" (isto é, "para a mente humana") é apenas uma list de things com alguns sinos e assobios?

Herdar de List<T> sempre inaceitável? Quando é aceitável? Porque porque não? O que um programador deve considerar ao decidir se herdará de List<T> ou não?


Um time de futebol não é uma lista de jogadores de futebol. Um time de futebol é composto de uma lista de jogadores de futebol!

Isso é logicamente errado:

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

e isso está correto:

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

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

Código anterior significa: um monte de caras da rua jogando futebol, e eles têm um nome. Algo como:

Enfim, esse código (da minha resposta)

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

Meios: este é um time de futebol que tem administração, jogadores, administradores, etc. Algo como:

É assim que sua lógica é apresentada em fotos ...


Se os usuários da sua turma precisarem de todos os métodos e propriedades ** Lista, você deverá derivar sua turma dela. Se eles não precisarem deles, coloque a Lista e crie invólucros para os métodos que os usuários da sua classe realmente precisam.

Esta é uma regra estrita, se você escrever uma API pública ou qualquer outro código que será usado por muitas pessoas. Você pode ignorar esta regra se tiver um aplicativo pequeno e não mais de dois desenvolvedores. Isso vai lhe poupar algum tempo.

Para aplicativos pequenos, você também pode considerar escolher outro idioma menos rigoroso. Ruby, JavaScript - qualquer coisa que permita escrever menos código.


Por último, alguns sugerem envolver a lista em algo:

Esse é o caminho correto. "Desnecessariamente wordy" é uma maneira ruim de ver isso. Tem um significado explícito quando você escreve my_team.Players.Count . Você quer contar os jogadores.

my_team.Count

..não significa nada. Conte o que?

Uma equipe não é uma lista - consiste em mais do que apenas uma lista de jogadores. Uma equipe possui jogadores, então os jogadores devem fazer parte dela (um membro).

Se você está realmente preocupado com o fato de ser excessivamente detalhado, é sempre possível expor propriedades da equipe:

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

..que se torna:

my_team.PlayerCount

Isso tem significado agora e adere à Lei de Demeter .

Você também deve considerar aderir ao princípio de reutilização Composite . Ao herdar de List<T> , você está dizendo que uma equipe é uma lista de jogadores e expõe métodos desnecessários dela. Isso é incorreto - como você afirmou, uma equipe é mais do que uma lista de jogadores: tem um nome, gerentes, membros do conselho, treinadores, equipe médica, limites salariais, etc. Por ter sua classe de equipe contendo uma lista de jogadores, você está dizendo "Uma equipe tem uma lista de jogadores", mas também pode ter outras coisas.


Depende do comportamento do seu objeto "equipe". Se ele se comporta como uma coleção, pode ser correto representá-lo primeiro com uma lista simples. Então você pode começar a notar que você continua duplicando o código que itera na lista; Neste ponto, você tem a opção de criar um objeto do FootballTeam que encapsula a lista de jogadores. A classe FootballTeam se torna o lar de todo o código que interage na lista de jogadores.

Isso torna meu código desnecessariamente detalhado. Agora devo chamar my_team.Players.Count em vez de apenas my_team.Count. Felizmente, com o C #, posso definir indexadores para tornar a indexação transparente e encaminhar todos os métodos da List interna ... Mas isso é muito código! O que eu ganho com todo esse trabalho?

Encapsulamento. Seus clientes não precisam saber o que acontece dentro do FootballTeam. Para todos os seus clientes sabem, pode ser implementado olhando a lista de jogadores em um banco de dados. Eles não precisam saber e isso melhora seu design.

Simplesmente não faz nenhum sentido. Um time de futebol não "tem" uma lista de jogadores. É a lista de jogadores. Você não diz "John McFootballer se juntou aos jogadores da SomeTeam". Você diz "John se juntou a SomeTeam". Você não adiciona uma letra a "caracteres de uma string", você adiciona uma letra a uma string. Você não adiciona um livro aos livros de uma biblioteca e adiciona um livro a uma biblioteca.

Exatamente :) você dirá footballTeam.Add (john), não footballTeam.List.Add (john). A lista interna não estará visível.


Há muitas respostas excelentes aqui, mas quero tocar em algo que não vi mencionado: Design orientado a objeto é sobre empoderamento de objetos .

Você quer encapsular todas as suas regras, trabalho adicional e detalhes internos dentro de um objeto apropriado. Desta forma, outros objetos interagindo com este não precisam se preocupar com tudo isso. Na verdade, você quer dar um passo adiante e impedir ativamente que outros objetos ignorem essas partes internas.

Quando você herda List, todos os outros objetos podem vê-lo como uma lista. Eles têm acesso direto aos métodos para adicionar e remover jogadores. E você perdeu seu controle; por exemplo:

Suponha que você queira diferenciar quando um jogador sai sabendo se ele se aposentou, pediu demissão ou foi demitido. Você pode implementar um RemovePlayermétodo que use um enum de entrada apropriado. No entanto, herdando de List, você seria incapaz de impedir o acesso direto Remove, RemoveAlle até mesmo Clear. Como resultado, você realmente dispensou sua FootballTeamclasse.

Pensamentos adicionais sobre o encapsulamento ... Você levantou a seguinte preocupação:

Isso torna meu código desnecessariamente detalhado. Agora devo chamar my_team.Players.Count em vez de apenas my_team.Count.

Você está correto, isso seria desnecessariamente detalhado para todos os clientes usarem sua equipe. No entanto, esse problema é muito pequeno em comparação com o fato de você ter sido exposto List Playersa toda a gente para que eles possam mexer com sua equipe sem o seu consentimento.

Você continua dizendo:

Simplesmente não faz nenhum sentido. Um time de futebol não "tem" uma lista de jogadores. É a lista de jogadores. Você não diz "John McFootballer se juntou aos jogadores da SomeTeam". Você diz "John se juntou a SomeTeam".

Você está errado sobre o primeiro: Solte a palavra 'lista', e é óbvio que uma equipe tem jogadores.
No entanto, você bateu o prego na cabeça com o segundo. Você não quer clientes ligando ateam.Players.Add(...). Você quer que eles liguem ateam.AddPlayer(...). E sua implementação iria (possivelmente entre outras coisas) chamar Players.Add(...)internamente.

Espero que você possa ver o quão importante é o encapsulamento para o objetivo de capacitar seus objetos. Você quer permitir que cada classe faça bem seu trabalho sem medo da interferência de outros objetos.


Como todos apontaram, uma equipe de jogadores não é uma lista de jogadores. Este erro é cometido por muitas pessoas em todos os lugares, talvez em vários níveis de especialização. Muitas vezes o problema é sutil e ocasionalmente muito grosseiro, como neste caso. Tais projetos são ruins porque estes violam o Princípio de Substituição de Liskov . A internet tem muitos bons artigos explicando esse conceito, por exemplo, http://en.wikipedia.org/wiki/Liskov_substitution_principle

Em resumo, existem duas regras a serem preservadas em um relacionamento pai / filho entre classes:

  • Uma criança não deve exigir nenhuma característica menor do que o que define completamente o pai.
  • Um pai não deve exigir nenhuma característica além do que define completamente a criança.

Em outras palavras, um pai é uma definição necessária de um filho, e um filho é uma definição suficiente de um pai.

Aqui está uma maneira de pensar através da solução e aplicar o princípio acima que deve ajudar a evitar esse erro. Deve-se testar as hipóteses verificando se todas as operações de uma classe pai são válidas para a classe derivada estrutural e semanticamente.

  • Um time de futebol é uma lista de jogadores de futebol? (Todas as propriedades de uma lista se aplicam a uma equipe com o mesmo significado)
    • Uma equipe é uma coleção de entidades homogêneas? Sim, a equipe é uma coleção de jogadores
    • A ordem de inclusão dos jogadores é descritiva do estado da equipe e a equipe garante que a sequência seja preservada, a menos que seja explicitamente alterada? Não e não
    • Os jogadores devem ser incluídos / descartados com base em sua posição sequencial no time? Não

Como você vê, apenas a primeira característica de uma lista é aplicável a uma equipe. Por isso, uma equipe não é uma lista. Uma lista seria um detalhe de implementação de como você gerencia sua equipe, por isso só deve ser usado para armazenar os objetos do jogador e ser manipulado com métodos da classe Team.

Neste ponto, gostaria de observar que uma classe de equipe, na minha opinião, não deve ser implementada usando uma lista; ele deve ser implementado usando uma estrutura de dados Set (HashSet, por exemplo) na maioria dos casos.


Design> Implementação

Quais métodos e propriedades você expõe são uma decisão de design. De qual classe base você herda é um detalhe de implementação. Eu sinto que vale a pena dar um passo atrás para o primeiro.

Um objeto é uma coleção de dados e comportamento.

Então suas primeiras perguntas devem ser:

  • Quais dados esse objeto compõe no modelo que estou criando?
  • Qual comportamento esse objeto exibe nesse modelo?
  • Como isso pode mudar no futuro?

Tenha em mente que a herança implica um relacionamento "isa" (é a), enquanto a composição implica um relacionamento "has a" (hasa). Escolha o caminho certo para a sua situação na sua opinião, tendo em mente onde as coisas podem ir à medida que sua aplicação evolui.

Considere pensar em interfaces antes de pensar em tipos concretos, pois algumas pessoas acham mais fácil colocar seu cérebro no "modo de design" dessa maneira.

Isso não é algo que todos fazem conscientemente a esse nível no dia a dia da codificação. Mas se você está ponderando este tipo de assunto, você está pisando em águas de design. Estar ciente disso pode ser libertador.

Considere design específicos

Dê uma olhada no List <T> e IList <T> no MSDN ou no Visual Studio. Veja quais métodos e propriedades eles expõem. Todos estes métodos se parecem com algo que alguém gostaria de fazer para um FootballTeam na sua opinião?

O footballTeam.Reverse () faz sentido para você? O footballTeam.ConvertAll <TOutput> () parece com algo que você quer?

Esta não é uma pergunta complicada; a resposta pode ser genuinamente "sim". Se você implementar / herdar List <Player> ou IList <Player>, você está preso a eles; se isso é ideal para o seu modelo, faça isso.

Se você decidir sim, isso faz sentido, e você quer que seu objeto seja tratável como uma coleção / lista de jogadores (comportamento) e, portanto, você deseja implementar ICollection ou IList, por todos os meios fazê-lo. Notionalmente:

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

Se você quiser que o seu objeto contenha uma coleção / lista de jogadores (dados) e, portanto, deseja que a coleção ou lista seja uma propriedade ou um membro, faça isso de todas as maneiras. Notionalmente:

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

Você pode achar que deseja que as pessoas só possam enumerar o conjunto de jogadores, em vez de contá-las, adicioná-las ou removê-las. IEnumerable <Player> é uma opção perfeitamente válida a considerar.

Você pode achar que nenhuma dessas interfaces é útil em seu modelo. Isso é menos provável (IEnumerable <T> é útil em muitas situações), mas ainda é possível.

Qualquer um que tente dizer-lhe que um deles é categoricamente e definitivamente errado em todos os casos é equivocado. Qualquer um que tente lhe dizer é categoricamente e definitivamente certo em todos os casos é equivocado.

Mover para Implementação

Depois de decidir os dados e o comportamento, você pode tomar uma decisão sobre a implementação. Isso inclui quais classes concretas você depende de herança ou composição.

Isso pode não ser um grande passo, e as pessoas geralmente combinam design e implementação, já que é possível passar por tudo isso em sua cabeça em um segundo ou dois e começar a digitar.

Uma experiência de pensamento

Um exemplo artificial: como outros já mencionaram, uma equipe nem sempre é "apenas" uma coleção de jogadores. Você mantém uma coleção de pontuações de jogo para a equipe? A equipe é intercambiável com o clube em seu modelo? Se sim, e se sua equipe for uma coleção de jogadores, talvez seja também uma coleção de funcionários e / ou uma coleção de pontuações. Então você acaba com:

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

Design não obstante, neste ponto em C # você não será capaz de implementar tudo isso herdando de List <T> de qualquer maneira, já que o C # "only" suporta herança simples. (Se você já tentou este malarky em C ++, você pode considerar isso uma coisa boa.) Implementar uma recolha através de herança e uma via composição é susceptível de se sentir sujo. E propriedades como Count tornam-se confusas para os usuários, a menos que você implemente ILIst <Player> .Count e IList <StaffMember> .Count etc. explicitamente, e eles são apenas dolorosos e não confusos. Você pode ver para onde isso está indo; Por outro lado, pensar nisso, embora pense bem nesse caminho, pode lhe dizer que é errado seguir nessa direção (e, com razão ou sem razão, seus colegas também podem se você o implementou dessa maneira!)

A resposta curta (tarde demais)

A diretriz sobre não herdar de classes de coleções não é específica do C #, você encontrará em muitas linguagens de programação. É recebido sabedoria e não uma lei. Uma das razões é que, na prática, a composição é considerada, muitas vezes, uma herança hereditária em termos de compreensibilidade, implementação e sustentabilidade. É mais comum com objetos do mundo real / domínio encontrar relacionamentos "hasa" úteis e consistentes do que relacionamentos "isa" úteis e consistentes, a menos que você esteja no fundo, especialmente com o passar do tempo e os dados e comportamentos precisos dos objetos no código alterar. Isso não deve fazer com que você descarte sempre a herança de classes de coleção; mas pode ser sugestivo.


Eu só queria acrescentar que Bertrand Meyer, o inventor de Eiffel e design por contrato, teria Teamherdado de List<Player>sem sequer uma pálpebra.

Em seu livro, Construção de Software Orientado a Objetos , ele discute a implementação de um sistema de GUI onde janelas retangulares podem ter janelas filhas. Ele simplesmente Windowherdou de ambos Rectanglee Tree<Window>reutilizou a implementação.

No entanto, c # não é Eiffel. O último suporta herança múltipla e renomeação de recursos . Em C #, quando você subclasse, você herda a interface e a implementação. Você pode substituir a implementação, mas as convenções de chamada são copiadas diretamente da superclasse. Em Eiffel, no entanto, você pode modificar os nomes dos métodos públicos, para que você possa renomear Adde Removepara Hiree Fireno seu Team. Se uma instância Teamé upcast de volta para List<Player>o chamador vai usar Adde Removemodificá-lo, mas seus métodos virtuais Hiree Fireserá chamado.


Depende do contexto

Quando você considera sua equipe como uma lista de jogadores, você está projetando a "ideia" de um time de futebol de pé em um aspecto: Você reduz o "time" para as pessoas que você vê no campo. Essa projeção é correta apenas em um determinado contexto. Em um contexto diferente, isso pode estar completamente errado. Imagine que você quer se tornar um patrocinador da equipe. Então você tem que conversar com os gerentes da equipe. Nesse contexto, a equipe é projetada para a lista de seus gerentes. E essas duas listas geralmente não se sobrepõem muito. Outros contextos são os atuais contra os ex-jogadores, etc.

Semântica pouco clara

Portanto, o problema em considerar uma equipe como uma lista de seus jogadores é que sua semântica depende do contexto e que não pode ser estendida quando o contexto é alterado. Além disso, é difícil expressar qual contexto você está usando.

Classes são extensíveis

Quando você usa uma classe com apenas um membro (por exemplo IList activePlayers), você pode usar o nome do membro (e adicionalmente seu comentário) para tornar o contexto claro. Quando há contextos adicionais, você apenas adiciona um membro adicional.

As aulas são mais complexas

Em alguns casos, pode ser um exagero criar uma classe extra. Cada definição de classe deve ser carregada através do carregador de classe e armazenada em cache pela máquina virtual. Isso custa seu desempenho em tempo de execução e memória. Quando você tem um contexto muito específico, pode ser bom considerar um time de futebol como uma lista de jogadores. Mas neste caso, você deveria usar apenas IListuma classe, e não uma derivada dela.

Conclusão / Considerações

Quando você tem um contexto muito específico, não há problema em considerar uma equipe como uma lista de jogadores. Por exemplo, dentro de um método, é completamente correto escrever

IList<Player> footballTeam = ...

Ao usar o F #, pode até ser OK criar uma abreviação de tipo

type FootballTeam = IList<Player>

Mas quando o contexto é mais amplo ou mesmo incerto, você não deve fazer isso. Este é especialmente o caso, quando você cria uma nova classe, onde não está claro em qual contexto ela pode ser usada no futuro. Um sinal de aviso é quando você começa a adicionar atributos adicionais à sua classe (nome da equipe, técnico etc.). Esse é um sinal claro de que o contexto em que a classe será usada não será corrigido e será alterado no futuro. Neste caso, você não pode considerar a equipe como uma lista de jogadores, mas você deve modelar a lista dos jogadores (atualmente ativos, não feridos, etc.) como um atributo da equipe.


Preferir Interfaces sobre Classes

As classes devem evitar derivar de classes e, em vez disso, implementar as interfaces mínimas necessárias.

Quebras de herança Encapsulamento

Derivando de classes quebra o encapsulamento :

  • expõe detalhes internos sobre como sua coleção é implementada
  • declara uma interface (conjunto de funções e propriedades públicas) que pode não ser apropriada

Entre outras coisas, isso torna mais difícil refatorar seu código.

Classes são um detalhe de implementação

As classes são um detalhe de implementação que deve ser escondido de outras partes do seu código. Em suma, System.Listé uma implementação específica de um tipo de dados abstrato, que pode ou não ser apropriado agora e no futuro.

Conceitualmente, o fato de o System.Listtipo de dados ser chamado de "lista" é um pouco arriscado. A System.List<T>é uma coleção ordenada mutável que suporta operações O (1) amortizadas para adicionar, inserir e remover elementos e operações O (1) para recuperar o número de elementos ou obter e definir elemento por índice.

Quanto menor a interface, mais flexível é o código

Ao projetar uma estrutura de dados, quanto mais simples for a interface, mais flexível será o código. Basta ver como o LINQ é poderoso para uma demonstração disso.

Como escolher interfaces

Quando você pensa em "lista", você deve começar dizendo "Eu preciso representar uma coleção de jogadores de beisebol". Então, digamos que você decida modelar isso com uma classe. O que você deve fazer primeiro é decidir qual a quantidade mínima de interfaces que essa classe precisará expor.

Algumas perguntas que podem ajudar a orientar esse processo:

  • Preciso ter a contagem? Se não considerar implementarIEnumerable<T>
  • Esta coleção vai mudar depois de ter sido inicializada? Se não considerar IReadonlyList<T>.
  • É importante que eu possa acessar itens por índice? ConsiderarICollection<T>
  • A ordem em que adiciono itens à coleção é importante? Talvez seja um ISet<T>?
  • Se você realmente quer essas coisas, vá em frente e implemente IList<T>.

Desta forma, você não estará acoplando outras partes do código aos detalhes de implementação de sua coleção de jogadores de beisebol e estará livre para mudar a forma como ela é implementada, desde que você respeite a interface.

Ao adotar essa abordagem, você descobrirá que o código se torna mais fácil de ler, refatorar e reutilizar.

Notas sobre como evitar o clichê

Implementar interfaces em um IDE moderno deve ser fácil. Clique com o botão direito e escolha "Implementar interface". Em seguida, encaminhe todas as implementações para uma classe membro, se necessário.

Dito isso, se você perceber que está escrevendo muito clichê, é potencialmente porque você está expondo mais funções do que deveria. É a mesma razão pela qual você não deve herdar de uma aula.

Você também pode criar interfaces menores que façam sentido para seu aplicativo e talvez apenas algumas funções de extensão de ajuda para mapear essas interfaces para quaisquer outras que você precisar. Esta é a abordagem que fiz na minha própria IArrayinterface para a biblioteca LinqArray .


Quando eles dizem que List<T>é "otimizado", eu acho que eles querem dizer que ele não tem recursos como métodos virtuais que são um pouco mais caros. Portanto, o problema é que, quando você expõe List<T>sua API pública , perde a capacidade de impor regras comerciais ou personalizar sua funcionalidade posteriormente. Mas se você estiver usando essa classe herdada como interna em seu projeto (em oposição a potencialmente exposta a milhares de seus clientes / parceiros / outras equipes como API), pode ser OK se economizar seu tempo e for a funcionalidade que você deseja duplicado. A vantagem de herdar List<T>é que você elimina muito código de wrap idiota que nunca será personalizado em um futuro previsível. Além disso, se você quiser que sua classe tenha explicitamente a mesma semânticaList<T> para a vida de suas APIs, então também pode ser OK.

Muitas vezes vejo muitas pessoas fazendo toneladas de trabalho extra apenas porque a regra FxCop diz isso ou o blog de alguém diz que é uma prática "ruim". Muitas vezes, isso transforma o código em um padrão de design de estranheza palooza. Como com muita diretriz, trate como orientação que pode ter exceções.


O que as diretrizes dizem é que a API pública não deve revelar a decisão de design interno de se você está usando uma lista, um conjunto, um dicionário, uma árvore ou o que for. Uma "equipe" não é necessariamente uma lista. Você pode implementá-lo como uma lista, mas os usuários de sua API pública devem usar sua classe com base na necessidade de saber. Isso permite que você altere sua decisão e use uma estrutura de dados diferente sem afetar a interface pública.


Deixe-me reescrever sua pergunta. então você pode ver o assunto de uma perspectiva diferente.

Quando eu preciso representar um time de futebol, entendo que é basicamente um nome. Como: "as águias"

string team = new string();

Então mais tarde percebi que as equipes também têm jogadores.

Por que não posso simplesmente estender o tipo de string para que ele também tenha uma lista de players?

Seu ponto de entrada no problema é arbitrário. Tente pensar o que uma equipe tem (propriedades), não o que é .

Depois disso, você poderá ver se ele compartilha propriedades com outras classes. E pense em herança.


Primeiro de tudo, tem a ver com usabilidade. Se você usar herança, a classe Team irá expor o comportamento (métodos) que são projetados apenas para manipulação de objetos. Por exemplo, os AsReadOnly() ou CopyTo(obj) não fazem sentido para o objeto de equipe.Em vez do AddRange(items)método, você provavelmente desejaria um AddPlayers(players)método mais descritivo .

Se você quiser usar o LINQ, implementar uma interface genérica como ICollection<T>ou IEnumerable<T>faria mais sentido.

Como mencionado, a composição é o caminho certo para fazê-lo. Basta implementar uma lista de jogadores como uma variável privada.


Eu acho que não concordo com sua generalização. Uma equipe não é apenas uma coleção de jogadores. Uma equipe tem muito mais informações sobre isso - nome, emblema, coleção de equipe administrativa / administrativa, coleção de equipe técnica e, em seguida, coleção de jogadores. Então, corretamente, sua classe do FootballTeam deve ter 3 coleções e não ser uma coleção; se é para modelar adequadamente o mundo real.

Você pode considerar uma classe PlayerCollection que, como o StringCollection especializado, oferece alguns outros recursos - como validação e verificações antes que os objetos sejam adicionados ou removidos do armazenamento interno.

Talvez a noção de um apostador do PlayerCollection seja mais adequada?

public class PlayerCollection : Collection<Player> 
{ 
}

E então o FootballTeam pode ser assim:

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

Isso me lembra do "é um" versus "tem um" tradeoff. Às vezes é mais fácil e faz mais sentido herdar diretamente de uma superclasse. Outras vezes, faz mais sentido criar uma classe autônoma e incluir a classe da qual você teria herdado como variável de membro. Você ainda pode acessar a funcionalidade da classe, mas não está vinculado à interface ou a quaisquer outras restrições que possam vir da herança da classe.

O que você faz? Tal como acontece com muitas coisas ... depende do contexto. O guia que eu usaria é que, para herdar de outra classe, deveria haver um relacionamento "é um". Então, se você está escrevendo uma classe chamada BMW, ela pode herdar de um carro, porque um BMW realmente é um carro. Uma classe Horse pode herdar da classe Mamífero porque um cavalo é na verdade um mamífero na vida real e qualquer funcionalidade Mammal deve ser relevante para Horse. Mas você pode dizer que uma equipe é uma lista? Pelo que posso dizer, não parece que uma equipe realmente "é uma" lista. Então, neste caso, eu teria uma lista como uma variável de membro.


E se o FootballTeam tiver uma equipe de reservas junto com a equipe principal?

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

Como você modelaria isso com?

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

O relacionamento é claramente tem um e não é um .

ou RetiredPlayers ?

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

Como regra geral, se você quiser herdar de uma coleção, nomeie a classe como SomethingCollection .

O seu SomethingCollection semanticamente faz sentido? Só faça isso se o seu tipo for uma coleção de Something .

No caso do FootballTeam , não parece certo. Uma Team é mais que uma Collection . Uma Team pode ter treinadores, treinadores, etc, como as outras respostas apontaram.

FootballCollection parece uma coleção de bolas de futebol ou talvez uma coleção de parafernália de futebol. TeamCollection , uma coleção de equipes.

FootballPlayerCollection soa como uma coleção de jogadores que seria um nome válido para uma classe que herda de List<FootballPlayer> se você realmente quisesse fazer isso.

Really List<FootballPlayer> é um tipo perfeitamente bom de se lidar. Talvez IList<FootballPlayer> se você está retornando de um método.

Em suma

Pergunte a si mesmo

  1. X é um Y ? ou tem X a Y ?

  2. Os meus nomes de classe significam o que são?


Este é um exemplo clássico de composition versus inheritance .

Neste caso específico:

A equipe é uma lista de jogadores com comportamento adicionado

ou

A equipe é um objeto próprio que contém uma lista de jogadores?

Ao estender List, você está se limitando de várias maneiras:

  1. Você não pode restringir o acesso (por exemplo, parando as pessoas alterando a lista). Você obtém todos os métodos da Lista, quer precise / queira todos ou não.

  2. O que acontece se você quiser ter listas de outras coisas também. Por exemplo, as equipes têm treinadores, gerentes, torcedores, equipamentos, etc. Algumas delas podem ser listas por direito próprio.

  3. Você limita suas opções de herança. Por exemplo, você pode querer criar um objeto Team genérico e depois ter BaseballTeam, FootballTeam, etc., que herdam disso. Para herdar da Lista, você precisa fazer a herança da Equipe, mas isso significa que todos os vários tipos de equipe são obrigados a ter a mesma implementação dessa lista.

Composição - incluindo um objeto dando o comportamento que você deseja dentro do seu objeto.

Herança - seu objeto se torna uma instância do objeto que possui o comportamento desejado.

Ambos têm seus usos, mas este é um caso claro onde a composição é preferível.


Permitir que as pessoas digam

myTeam.subList(3, 5);

faz algum sentido? Se não, então não deveria ser uma lista.


Qual é a maneira C # correta de representar uma estrutura de dados ...

Lembre-se, "Todos os modelos estão errados, mas alguns são úteis". - George EP Box

Não existe um "caminho correto", apenas um útil.

Escolha um que seja útil para você e / para seus usuários. É isso aí. Desenvolva-se economicamente, não faça engenharia excessiva. Quanto menos código você escrever, menos código precisará depurar. (leia as seguintes edições).

- Editado

Minha melhor resposta seria ... depende. Herdar de uma Lista exporia os clientes desta classe a métodos que podem não ser expostos, principalmente porque o FootballTeam se parece com uma entidade de negócios.

- 2ª edição

Eu sinceramente não lembro o que eu estava me referindo no comentário "não engenhei demais". Embora eu acredite que a mentalidade KISS seja um bom guia, quero enfatizar que herdar uma classe de negócios da Lista criaria mais problemas do que resolve, devido ao vazamento de abstração .

Por outro lado, acredito que há um número limitado de casos em que simplesmente herdar da Lista é útil. Como escrevi na edição anterior, isso depende. A resposta para cada caso é fortemente influenciada pelo conhecimento, experiência e preferências pessoais.

Obrigado ao @kai por me ajudar a pensar com mais precisão sobre a resposta.







inheritance