desenv-web-rp.com

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

Quando planejo meus programas, muitas vezes começo com uma corrente de pensamento assim:

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, um 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 (um 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 similar 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 List<T> . Estou completamente confuso com 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 List? O que exatamente vai quebrar?

Outra razão que eu vi é que List é fornecido pela Microsoft, e eu não tenho controle sobre isso, então não posso alterá-lo 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 meu projeto atual não tem e provavelmente nunca terá essa API pública, posso ignorar com segurança essa diretriz? Se eu herdar List e acontece que eu preciso 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 fim, se a Microsoft não quisesse que eu herdasse de List, por que eles não criaram a classe sealed?

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. resposta de jvitor83 fornece uma lógica de desempenho para esse método em particular, mas como é que um AddRange não é melhor que não AddRange?

Herdar de Collection é muito mais trabalho do que herdar de 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 Collection não é a solução certa para o meu problema.

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

Por fim, alguns sugerem que o List seja colocado 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 em vez de apenas my_team.Count. Felizmente, com C # eu posso definir indexadores para tornar a indexação transparente, e encaminhar todos os métodos internos List... Mas isso é muito código! O que eu ganho com todo esse trabalho?

  2. Simplesmente não faz 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 aos "caracteres de uma string", você adiciona uma letra a uma string. Você não adiciona um livro aos livros de uma biblioteca, você 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 um list de things com alguns sinos e assobios?

A herança 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?

1184
Superbest

Existem algumas boas respostas aqui. Eu adicionaria a eles os seguintes pontos.

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

Pergunte a qualquer pessoa não programadora de computador que esteja familiarizada com a existência do futebol para preencher o espaço em branco:

A football team is a particular kind of _____

Did anyone say "lista de jogadores de futebol com alguns sinos e assobios", ou todos eles disseram "time esportivo" ou "clube" ou "organização"? Sua noção de que um time de futebol é um tipo particular de lista de jogadores está em sua mente humana e apenas em sua mente humana.

List<T> é um mechanism . A equipe de futebol é um business object - ou seja, um objeto que representa algum conceito que está no domínio business do programa. Não misture esses! Um time de futebol é uma espécie de time; it tem um lista, uma lista é uma lista de jogadores . Uma lista não é um tipo particular de lista de jogadores . Uma lista é uma lista de jogadores. Portanto, crie uma propriedade chamada Roster que seja List<Player>. E faça ReadOnlyList<Player> enquanto estiver nisso, a menos que você acredite que todos que conhecem um time de futebol excluam os jogadores da lista.

A herança de List<T> é sempre inaceitável?

Inaceitável para quem? Eu? Não.

Quando é aceitável?

Quando você está construindo um mecanismo que estende o mecanismo List<T>.

O que um programador deve considerar ao decidir se herdará de List<T> ou não?

Estou construindo um mecanismo ou um objeto de negócios ?

Mas isso é muito código! O que eu ganho com todo esse trabalho?

Você gastou mais tempo digitando sua pergunta que teria levado você a escrever métodos de encaminhamento para os membros relevantes de List<T> cinquenta vezes. Você claramente não tem medo de verbosidade, e estamos falando de uma pequena quantidade de código aqui; Este é um trabalho de alguns minutos.

ATUALIZAR

Pensei um pouco mais e há outro motivo para não modelar um time de futebol como uma lista de jogadores. Na verdade, pode ser uma má ideia modelar um time de futebol como tendo uma lista de jogadores também. O problema com uma equipe como/tendo uma lista de jogadores é que o que você tem é um instantâneo da equipe em um momento no tempo . Eu não sei qual é o seu caso de negócio para esta classe, mas se eu tivesse uma aula que representasse um time de futebol, eu gostaria de fazer perguntas como "quantos jogadores do Seahawks perderam jogos devido a lesão entre 2003 e 2013?" ou "Que jogador do Denver que jogou anteriormente para outro time teve o maior aumento ano a ano nos jardas?" ou " Os Piggers percorreram todo o caminho este ano? "

Ou seja, um time de futebol me parece bem modelado como uma coleção de fatos históricos como quando um jogador foi recrutado, ferido, aposentado, etc. Obviamente, a lista atual de jogadores é um fato importante que deve provavelmente, seja frontal e central, mas pode haver outras coisas interessantes que você deseja fazer com esse objeto que exigem uma perspectiva mais histórica.

1352
Eric Lippert

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ê estiver realmentepreocupado em ser excessivamente detalhado, você sempre pode expor propriedades da equipe:

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

..que se torna:

my_team.PlayerCount

Isso tem significado agora e adere a The Law Of Demeter .

Você também deve considerar aderir ao princípio de reutilização Composite . Ao herdar de List<T>, você está dizendo uma equipe é uma lista de jogadores e expondo 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.

272
Simon Whitehead

Uau, seu post tem uma enorme quantidade de perguntas e pontos. A maior parte do raciocínio que você recebe da Microsoft é exatamente no ponto. Vamos começar com tudo sobre List<T>

  • List<T> é altamente otimizado. Seu principal uso é ser usado como membro particular de um objeto.
  • A Microsoft não o fechou porque às vezes você pode querer criar uma classe que tenha um nome mais amigável: class MyList<T, TX> : List<CustomObject<T, Something<TX>> { ... }. Agora é tão fácil quanto fazer var list = new MyList<int, string>();.
  • CA1002: Não exponha listas genéricas : Basicamente, mesmo se você planeja usar este aplicativo como o único desenvolvedor, vale a pena desenvolver com boas práticas de codificação, para que elas sejam incutidas em você e em uma segunda natureza. Você ainda tem permissão para expor a lista como um IList<T> se precisar que algum consumidor tenha uma lista indexada. Isso permite que você mude a implementação dentro de uma classe mais tarde.
  • A Microsoft fez Collection<T> muito genérico porque é um conceito genérico ... o nome diz tudo; é apenas uma coleção. Existem versões mais precisas, como SortedCollection<T>, ObservableCollection<T>, ReadOnlyCollection<T>, etc. cada uma delas implementa IList<T> mas not List<T>.
  • Collection<T> permite que os membros (ou seja, Adicionar, Remover, etc.) sejam substituídos porque são virtuais. List<T> não.
  • A última parte da sua pergunta está no local. Um time de futebol é mais do que apenas uma lista de jogadores, então deve ser uma classe que contenha essa lista de jogadores. Pense Composição vs Herança . Um time de futebol tem uma lista de jogadores (uma lista), não é uma lista de jogadores.

Se eu estivesse escrevendo esse código, a classe provavelmente seria algo assim:

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.
}
131
m-y
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:

People playing football

De qualquer forma, esse código (da resposta 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.
}

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

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

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

123
Nean Der Thal

Este é um exemplo clássico de composition vs 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, fãs, 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 em que a composição é preferível.

100
Tim B

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 Liskov . A internet tem muitos bons artigos explicando este 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 requerer 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 tanto estrutural quanto 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 assegura 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 deveria ser implementada usando uma lista; ele deve ser implementado usando uma estrutura de dados Set (HashSet, por exemplo) na maioria dos casos.

59
Satyan Raina

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 
}

A relação é 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 SomethingCollection.

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

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

FootballCollection soa como 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.

Realmente List<FootballPlayer> é um tipo perfeitamente bom de 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?

45
Sam Leach

Design> Implementação

Quais métodos e propriedades você expõe é 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 todo mundo faz 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 capciosa; a resposta pode ser genuinamente "sim". Se você implementar/herdar List <Player> ou IList <Player>, ficará 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 o mesmo por todos os meios. 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 sentir 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 destes é categoricamente e definitivamente errado em todos os casos é equivocado. Qualquer um que tente lhe dizer é categoricamente e definitivamente certo em todo caso é 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 confundem design e implementação, já que é bem possível passar tudo isso em sua mente 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 é 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 momento 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 coleção via herança e uma via composição provavelmente 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 em vez de confusos. Você pode ver para onde isso está indo; Por outro lado, se você pensar nisso, você pode achar que é errado seguir nessa direção (e, com razão ou nã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 uma questão de herança em termos de inteligibilidade, implementabilidade 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.

33
El Zorko

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 métodos AsReadOnly() ou CopyTo(obj) não fazem sentido para o objeto de equipe. Em vez do método AddRange(items), você provavelmente desejaria um método AddPlayers(players) 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.

31
Dmitry S.

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 
}
26
Mauro Sampietro

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

Quando 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 contenha 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.

23
user1852503

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. Neste 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 antigos 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 não pode ser estendida quando o contexto muda. Além disso, é difícil expressar qual contexto você está usando.

Classes são extensíveis

Quando você usa uma turma com apenas um membro (por exemplo, IList activePlayers), você pode usar o nome do membro (e também seu comentário) para deixar 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ê deve usar apenas uma IList, não uma classe 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 é fixo 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.

22
stefan.schwetschke

Só porque eu acho que as outras respostas praticamente se importam em saber se um time de futebol "é-um" List<FootballPlayer> ou "has-a" List<FootballPlayer>, o que realmente não responde a essa pergunta como está escrito.

O OP pede principalmente esclarecimentos sobre as diretrizes para herdar de List<T>:

Uma diretriz diz que você não deve herdar List<T>. Por que não?

Porque List<T> não possui métodos virtuais. Isso é um problema menor em seu próprio código, já que você pode geralmente mudar a implementação com relativamente pouca dor - mas pode ser um problema muito maior em uma API pública.

O que é uma API pública e por que devo me importar?

Uma API pública é uma interface que você expõe a programadores de terceiros. Pense no código do framework. E lembre-se de que as diretrizes que estão sendo referenciadas são o ".NET Framework Design Guidelines" e não as ".NET Application Design Guidelines". Existe uma diferença e, em geral, o design da API pública é muito mais rigoroso.

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

Praticamente sim. Você pode considerar a lógica por trás dele para ver se ela se aplica à sua situação, mas se você não estiver criando uma API pública, não precisará se preocupar com as preocupações da API, como controle de versão (das quais, isso é subconjunto).

Se você adicionar uma API pública no futuro, precisará abstrair sua API de sua implementação (não expondo seu List<T> diretamente) ou violar as diretrizes com a possível dor futura que isso acarreta.

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

Depende do contexto, mas como estamos usando FootballTeam como exemplo - imagine que você não pode adicionar uma FootballPlayer se isso fizer com que a equipe ultrapasse o teto salarial. Uma maneira possível de adicionar isso seria 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 ... mas você não pode override Add porque não é virtual (por motivos de desempenho).

Se você estiver em um aplicativo (o que, basicamente, significa que você e todos os seus chamadores são compilados juntos), você pode agora mudar para IList<T> e consertar qualquer erro de compilação:

 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 */
 }

mas, se você expôs publicamente a uma terceira parte, você acabou de fazer uma alteração que causará erros de compilação e/ou de tempo de execução.

TL; DR - as diretrizes são para APIs públicas. Para APIs privadas, faça o que quiser.

18
Mark Brackett

Permitir que as pessoas digam

myTeam.subList(3, 5);

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

16
Cruncher

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 # eu 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 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 aos "caracteres de uma string", você adiciona uma letra a uma string. Você não adiciona um livro aos livros de uma biblioteca, você 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.

15
xpmatteo

Há muitas respostas excelentes aqui, mas quero tocar em algo que não vi mencionado: Design orientado a objetos é 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. De fato, você quer dar um passo adiante e ativamente impedir outros objetos de contornar esses elementos internos.

Quando você herda de 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 método RemovePlayer que use um enum de entrada apropriado. No entanto, ao herdar de List, você não poderá impedir o acesso direto a Remove, RemoveAll e até Clear. Como resultado, você realmente disempowered sua classe FootballTeam.


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 exposto List Players a todos e a outros para que possam mexer com sua equipe sem o seu consentimento.

Você continua dizendo:

Simplesmente não faz 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 bit: Solte a "lista" do Word, e é óbvio que um time tem jogadores.
No entanto, você acertou na unha na cabeça com o segundo. Você não quer clientes chamando ateam.Players.Add(...). Você quer que eles chamem ateam.AddPlayer(...). E sua implementação (possivelmente entre outras coisas) chamaria 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 de interferências de outros objetos.

13
Disillusioned

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

Lembre-se, "Todos os modelos estão errados, mas alguns são úteis". - George E. P. 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 não deveriam 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 o KISS mindset seja um bom guia, quero enfatizar que herdar uma classe de negócios da List criaria mais problemas do que resolve, devido a abstraction leakage .

Por outro lado, acredito que haja 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.

12
ArturoTena

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 do carro, porque uma 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.

10
Paul J Abernathy

Meu segredo sujo: eu não me importo com o que as pessoas dizem, e eu faço isso. O .NET Framework é distribuído com "XxxxCollection" (UIElementCollection para o exemplo da parte superior da minha cabeça).

Então, o que me impede de dizer:

team.Players.ByName("Nicolas")

Quando eu acho melhor que

team.ByName("Nicolas")

Além disso, meu PlayerCollection pode ser usado por outra classe, como "Club", sem qualquer duplicação de código.

club.Players.ByName("Nicolas")

As melhores práticas de ontem podem não ser as de amanhã. Não há razão por trás da maioria das melhores práticas, a maioria é apenas ampla concordância entre a comunidade. Em vez de perguntar à comunidade se ela vai culpá-lo quando fizer isso, pergunte a si mesmo: o que é mais legível e sustentável?

team.Players.ByName("Nicolas") 

ou

team.ByName("Nicolas")

Mesmo. Você tem alguma dúvida? Agora talvez você precise jogar com outras restrições técnicas que impedem que você use List <T> em seu caso de uso real. Mas não adicione uma restrição que não deveria existir. Se a Microsoft não documentou o porquê, então é certamente uma "melhor prática" vinda do nada.

9
Nicolas Dorier

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

8
QuentinUK

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, depois de expor List<T> em sua API public , você 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 exatamente a mesma semântica que List<T> durante a vida útil das suas APIs, também poderá ser OK.

Muitas vezes eu vejo muitas pessoas fazendo toneladas de trabalho extra só 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 de palooza. Tal como acontece com muita orientação, tratá-lo como diretriz que can tem exceções.

6
Shital Shah

Embora eu não tenha uma comparação complexa, como a maioria dessas respostas, gostaria de compartilhar meu método para lidar com essa situação. Ao estender IEnumerable<T>, você pode permitir que sua classe Team ofereça suporte a extensões de consulta Linq, sem expor publicamente todos os métodos e propriedades 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
{
    ...
}
4
Null511

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

Esta é uma regra estrita, se você escrever uma API public , 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.

3
Ivan Nikitin

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

Em seu livro, Construção de Software Orientado a Objetos , ele discute a implementação de um sistema GUI onde janelas retangulares podem ter janelas filho. Ele simplesmente tem Window herdado de Rectangle e Tree<Window> para reutilizar 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 Add e Remove para Hire e Fire em seu Team. Se uma instância de Team for upcast de volta para List<Player>, o chamador usará Add e Remove para modificá-lo, mas seus métodos virtuais Hire e Fire serão chamados.

3
Alexey

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 turma 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 se adapte à sua abordagem preferida?

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

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

Classes são um detalhe de implementação que deve ser escondido de outras partes do seu código. Em suma, um 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 que o tipo de dados System.List é chamado de "lista" é um pouco arriscado. Um System.List<T> é uma coleção ordenada mutável que suporta operações amortizadas O(1) para adicionar, inserir e remover elementos, e O(1) operações para recuperar o número de elementos ou obter e definindo 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:

  • Eu preciso ter a contagem? Se não considerar a implementação de IEnumerable<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? Considere ICollection<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> .

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

A implementação de interfaces em um IDEmoderno _ deve ser fácil. Clique com o botão direito e escolha "Implementar interface". Em seguida, encaminhe todas as implementações para uma classe de 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 auxiliar para mapear essas interfaces para quaisquer outras que você precisar. Esta é a abordagem que fiz na minha própria interface IArray para a biblioteca LinqArray .

0
cdiggins