Por que não herdar da Lista <T>?


1400

Ao planejar meus programas, geralmente começo com uma cadeia de pensamento assim:

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

var football_team = new List<FootballPlayer>();

A ordem desta lista representa a ordem em que os jogadores estão listados na lista.

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

Então eu penso:

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

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

Mas acontece que uma diretriz diz que você não deve herdarList<T> . Estou completamente confuso com esta diretriz em dois aspectos.

Por que não?

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

Outro motivo que eu vi é que Listé fornecido pela Microsoft, e eu não tenho controle sobre ele, então não posso alterá-lo mais tarde, depois de expor uma "API pública" . Mas luto para entender isso. O que é uma API pública e por que devo me importar? Se meu projeto atual não tiver e provavelmente não possuir essa API pública, posso ignorar com segurança esta diretriz? Se eu herdar List e precisar 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, finalmente, se a Microsoft não queria que eu herdasse List, por que eles não fizeram parte da classe sealed?

O que mais devo usar?

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

Herdar de Collectioné muito mais trabalho do que herdar List, e não vejo benefício. Certamente a Microsoft não me diria para fazer um trabalho extra sem motivo, por isso não posso deixar de sentir que estou de alguma forma entendendo algo, e herdar Collectionnão é realmente a solução certa para o meu problema.

Já vi sugestões como implementar IList. Apenas não. São dezenas de linhas de código padrão que não me valem nada.

Por fim, alguns sugerem envolver o Listitem em algo:

class FootballTeam 
{ 
    public List<FootballPlayer> Players; 
}

Existem dois problemas com isso:

  1. Isso torna meu código desnecessariamente detalhado. Agora devo ligar em my_team.Players.Countvez 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 do interno List... Mas isso é muito código! O que eu ganho por todo esse trabalho?

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

Percebo que o que acontece "sob o capô" pode ser considerado "acrescentando X à lista interna de Y", mas isso parece uma maneira muito contra-intuitiva de pensar sobre o mundo.

Minha pergunta (resumida)

Qual é a maneira C # correta de representar uma estrutura de dados, que "logicamente" (isto é, "para a mente humana") é apenas uma listdas thingspoucas 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 deve herdar List<T>ou não?


90
Então, sua pergunta é realmente quando usar herança (um quadrado é um retângulo porque é sempre razoável fornecer um quadrado quando um retângulo genérico foi solicitado) e quando usar a composição (um time de futebol tem uma lista de jogadores de futebol, além de para outras propriedades que são tão "fundamentais", como um nome)? Outros programadores ficariam confusos se em outro lugar do código você lhes passasse um FootballTeam quando esperavam uma lista simples?
Flight Odyssey

77
e se eu disser que um time de futebol é uma empresa comercial que possui uma lista de contratos de jogadores em vez de uma lista simples de jogadores com algumas propriedades adicionais?
STT LCU

75
É realmente bem simples. Um time de futebol é uma lista de jogadores? Obviamente não, porque, como você diz, existem outras propriedades relevantes. Um time de futebol tem uma lista de jogadores? Sim, portanto, deve conter uma lista. Fora isso, a composição é preferível à herança em geral, porque é mais fácil modificar posteriormente. Se você achar herança e composição lógicas ao mesmo tempo, vá com a composição.
Tobberoth

45
@FlightOdyssey: Suponha que você tenha um método SquashRectangle que retire um retângulo, reduza pela metade sua altura e dobre sua largura. Agora passar em um retângulo que acontece a ser 4x4, mas é do tipo retângulo e um quadrado que é 4x4. Quais são as dimensões do objeto depois que o SquashRectangle é chamado? Para o retângulo, claramente é 2x8. Se o quadrado é 2x8, não é mais um quadrado; se não for 2x8, a mesma operação na mesma forma produzirá um resultado diferente. A conclusão é que um quadrado mutável não é um tipo de retângulo.
Eric Lippert

29
@ Gangnus: Sua afirmação é simplesmente falsa; uma subclasse real é necessária para ter uma funcionalidade que é um superconjunto da superclasse. A stringé necessária para fazer tudo o que se objectpode fazer e muito mais .
Eric Lippert

Respostas:


1567

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

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

Peça a dez pessoas que não são programadoras de computador que estão familiarizadas com a existência do futebol para preencher o espaço em branco:

Um time de futebol é um tipo particular de _____

Será que alguém diga "lista de jogadores de futebol com alguns sinos e assobios", ou fizeram tudo o que "equipe de esportes" dizer ou "clube" ou "organização"? Sua noção de que um time de futebol é um tipo específico de lista de jogadores está na sua mente humana e somente na sua mente humana.

List<T>é um mecanismo . O time de futebol é um objeto de negócios - ou seja, um objeto que representa algum conceito que está no domínio de negócios do programa. Não misture isso! Um time de futebol é um tipo de time; ele tem uma lista, uma lista é uma lista de jogadores . Uma lista não é um tipo específico de lista de jogadores . Uma lista é uma lista de jogadores. Então faça uma propriedade chamada Rosterque é a List<Player>. E faça isso ReadOnlyList<Player>enquanto estiver fazendo isso, a menos que você acredite que todo mundo que conhece um time de futebol possa excluir 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 List<T>mecanismo .

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

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

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

Você gastou mais tempo digitando sua pergunta do que seria necessário escrever métodos de encaminhamento para os membros relevantes de mais de List<T>cinquenta vezes. Você claramente não tem medo de verbosidade, e estamos falando de uma quantidade muito pequena de código aqui; isso é alguns minutos de trabalho.

ATUALIZAR

Pensei um pouco mais e há outra razão para não modelar um time de futebol como uma lista de jogadores. De fato, pode ser uma má idéia modelar um time de futebol como tendo uma lista de jogadores também. O problema com uma equipe como / ter uma lista de jogadores é que o que você tem é um instantâneo da equipe em um momento no tempo . Não sei qual é o seu argumento comercial para esta classe, mas se eu tivesse uma classe 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 "Qual jogador de Denver que jogou anteriormente para outro time teve o maior aumento ano-a-ano em jardas?" ou "Os Piggers foram até o fim deste ano? "

Ou seja, um time de futebol parece-me bem modelado como uma coleção de fatos históricos , como quando um jogador foi recrutado, lesionado, aposentado, etc. Obviamente, a lista de jogadores atual é um fato importante que provavelmente deveria estar na frente centro, mas pode haver outras coisas interessantes que você deseja fazer com esse objeto que exijam uma perspectiva mais histórica.


84
Embora as outras respostas tenham sido muito úteis, acho que esta aborda minhas preocupações mais diretamente. Quanto a exagerar a quantidade de código, você está certo de que, no final, não dá muito trabalho, mas fico confuso com muita facilidade quando incluo algum código no meu programa sem entender por que o incluí.
Superbest

128
@Superbest: Ainda bem que pude ajudar! Você está certo em ouvir essas dúvidas; se você não entender por que está escrevendo algum código, descubra ou escreva um código diferente que você entenda.
Eric Lippert

48
@ Mehrdad Mas você parece esquecer as regras de ouro da otimização: 1) não faça isso, 2) não faça ainda e 3) não faça isso sem antes fazer perfis de desempenho para mostrar o que precisa ser otimizado.
AJMansfield

107
@ Mehrdad: Honestamente, se seu aplicativo exigir que você se preocupe com a carga de desempenho de, digamos, métodos virtuais, qualquer linguagem moderna (C #, Java, etc) não é a linguagem para você. Eu tenho um laptop aqui que pode executar uma simulação de todos os jogos de arcade que eu joguei quando criança simultaneamente . Isso me permite não me preocupar com um nível de indireção aqui e ali.
Eric Lippert

44
@Superbest: Agora estamos definitivamente no final da "composição não herança" do espectro. Um foguete é composto de partes de foguetes . Um foguete não é "um tipo especial de lista de peças"! Eu sugeriria que você aparasse ainda mais; por que uma lista, em oposição a uma IEnumerable<T>- uma sequência ?
Eric Lippert

161

Uau, sua postagem tem uma série de perguntas e pontos. A maior parte do raciocínio que você obtém da Microsoft está exatamente no ponto. Vamos começar com tudo sobreList<T>

  • List<T> é altamente otimizado. Seu principal uso é para ser usado como um membro privado de um objeto.
  • Microsoft não selá-lo, porque às vezes você pode querer criar uma classe que tem 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 desenvolvedor exclusivo, vale a pena desenvolver com boas práticas de codificação, para que elas sejam instiladas em você e em uma segunda natureza. Você ainda pode expor a lista como IList<T>se você precisar que algum consumidor tenha uma lista indexada. Isso permite alterar a implementação dentro de uma classe posteriormente.
  • A Microsoft tornou Collection<T>muito genérica porque é um conceito genérico ... o nome já diz tudo; é apenas uma coleção. Existem versões mais precisas, como SortedCollection<T>, ObservableCollection<T>, ReadOnlyCollection<T>, etc. cada um dos quais implementam IList<T>, mas não List<T> .
  • Collection<T>permite que os membros (por exemplo, Adicionar, Remover, etc.) sejam substituídos por serem 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; portanto, deve ser uma classe que contém 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 ficaria 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.
}

6
"o seu post tem um monte de perguntas " - Bem, eu disse que estava completamente confuso. Obrigado por vincular à pergunta de @ Readonly, agora vejo que grande parte da minha pergunta é um caso especial do problema mais geral de Composição vs. Herança.
Superbest

3
@ Brian: Exceto que você não pode criar um genérico usando alias, todos os tipos devem ser concretos. No meu exemplo, List<T>é estendida como uma classe interna privada que usa genéricos para simplificar o uso e a declaração de List<T>. Se a extensão fosse meramente class FootballRoster : List<FootballPlayer> { }, então sim, eu poderia ter usado um apelido. Eu acho que vale a pena notar que você pode adicionar membros / funcionalidade adicionais, mas é limitado, pois você não pode substituir membros importantes de List<T>.
o meu

6
Se esse fosse o Programmers.SE, eu concordaria com a faixa atual de votos nas respostas, mas, como se trata de uma postagem no SO, essa resposta pelo menos tenta responder a questões específicas do C # (.NET), mesmo que haja definições gerais definidas. questões de design.
Mark Hurd

7
Collection<T> allows for members (i.e. Add, Remove, etc.) to be overriddenAgora, esse é um bom motivo para não herdar da Lista. As outras respostas têm filosofia demais.
Navin

10
@my: Eu suspeito que você quer dizer "muitas" perguntas, já que um detetive é um detetive.
extravagantemente

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

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

Pessoas jogando futebol

Enfim, esse código (da minha resposta)

public class FootballTeam
{
    // A team's name
    public string TeamName; 

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

Significa: trata-se de um time de futebol que possui dirigentes, jogadores, administradores, etc. Algo como:

Imagem mostrando membros (e outros atributos) da equipe do Manchester United

É assim que a sua lógica é apresentada nas fotos ...


9
Eu esperaria que o seu segundo exemplo seja um clube de futebol, não um time de futebol. Um clube de futebol tem gerentes e administradores, etc. A partir desta fonte : "Um time de futebol é o nome coletivo dado a um grupo de jogadores ... Esses times podem ser selecionados para disputar uma partida contra um time adversário, para representar um clube de futebol". ... " Portanto, é minha opinião que um time seja uma lista de jogadores (ou talvez mais precisamente uma coleção de jogadores).
Ben

3
Mais otimizadoprivate readonly List<T> _roster = new List<T>(44);
SiD

2
É necessário importar o System.Linqespaço para nome.
Davide Cannizzo

1
Para efeitos visuais: +1
V.7

115

Este é um exemplo clássico de composição versus herança .

Nesse caso específico:

O time é uma lista de jogadores com comportamento adicional

ou

O time é um objeto próprio que contém uma lista de jogadores.

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

  1. Você não pode restringir o acesso (por exemplo, impedir que as pessoas alterem a lista). Você obtém todos os métodos de lista, independentemente de precisar ou querer todos eles.

  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. Alguns deles podem ser listas por si só.

  3. Você limita suas opções de herança. Por exemplo, você pode criar um objeto Team genérico e, em seguida, ter BaseballTeam, FootballTeam etc. que herdem 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 forçados a ter a mesma implementação dessa lista.

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

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

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


1
Expandindo o número 2, não faz sentido que uma lista tenha uma lista.
triturador

Primeiro, a questão não tem nada a ver com composição versus herança. A resposta é que o OP não deseja implementar um tipo mais específico de lista, portanto ele não deve estender a Lista <>. Estou perplexo porque você tem pontuações muito altas no stackoverflow e deve saber isso claramente e as pessoas confiam no que você diz, então, agora, um mínimo de 55 pessoas que votaram e cuja ideia está confusa acredita que, de um jeito ou de outro, está tudo bem em construir um sistema, mas claramente não é! # no-rage #
Mauro Sampietro

6
@ Sam Ele quer o comportamento da lista. Ele tem duas opções, pode estender a lista (herança) ou pode incluir uma lista dentro de seu objeto (composição). Talvez você tenha entendido errado parte da pergunta ou da resposta, em vez de 55 pessoas estarem erradas e você está certo? :)
Tim B

4
Sim. É um caso degenerado com apenas um super e sub, mas estou dando uma explicação mais geral para sua pergunta específica. Ele pode ter apenas uma equipe e isso sempre pode ter uma lista, mas a opção ainda é herdar a lista ou incluí-la como um objeto interno. Depois de começar a incluir vários tipos de equipe (futebol, críquete etc.) e eles começam a ser mais do que apenas uma lista de jogadores, você vê como tem a questão da composição completa versus herança. Observar o cenário geral é importante em casos como esse para evitar escolhas erradas no início, o que significa muita refatoração posteriormente.
Tim B

3
O OP não pediu Composição, mas o caso de uso é um exemplo claro do problema X: Y, no qual um dos quatro princípios de programação orientada a objetos é quebrado, mal utilizado ou não totalmente compreendido. A resposta mais clara é escrever uma coleção especializada como uma pilha ou fila (que não parece se encaixar no caso de uso) ou entender a composição. Um time de futebol NÃO é uma lista de jogadores de futebol. Se o OP pediu explicitamente ou não é irrelevante, sem entender abstração, encapsulamento, herança e polimorfismo eles não vão entender a resposta, ergo, X: Y amok
Julia McGuigan

67

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 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, há 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 sobre a solução e aplicar o princípio acima, que deve ajudar a evitar esse erro. Deve-se testar a hipótese 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 no 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 garante que a sequência seja preservada, a menos que seja explicitamente alterada? Não e não
    • Espera-se que os jogadores sejam 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. Portanto, uma equipe não é uma lista. Uma lista seria um detalhe de implementação de como você gerencia sua equipe, portanto, deve ser usada apenas para armazenar os objetos do jogador e ser manipulada com métodos da classe Equipe.

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


8
Boa captura na lista versus conjunto. Isso parece ser um erro muito comum. Quando só pode haver uma instância de um elemento em uma coleção, algum tipo de conjunto ou dicionário deve ser um candidato preferido para a implementação. Não há problema em ter um time com dois jogadores com o mesmo nome. Não é permitido incluir um jogador duas vezes ao mesmo tempo.
115514 Fred Mitchell

1
+1 para alguns pontos positivos, embora seja mais importante perguntar "uma estrutura de dados pode suportar razoavelmente tudo o que é útil (idealmente, a curto e longo prazo)", em vez de "a estrutura de dados faz mais do que é útil".
Tony Delroy

1
@ TonyD Bem, o que estou levantando não é que se deva verificar "se a estrutura de dados faz mais do que é útil". É para verificar "Se a estrutura de dados pai faz algo que é irrelevante, sem sentido ou contra-intuitivo para o comportamento que a classe Child pode implicar".
Satyan Raina

1
@TonyD Na verdade, há um problema em ter características irrelevantes derivadas de um pai, pois isso falharia nos testes negativos em muitos casos. Um programador pode estender Human {eat (); corre(); write ();} de um gorila {eat (); corre(); swing ();} pensando que não há nada de errado com um ser humano com um recurso extra de poder girar. E então, em um mundo de jogo, seu ser humano repentinamente começa a contornar todos os supostos obstáculos à terra, apenas balançando sobre as árvores. A menos que seja explicitamente especificado, um humano prático não deve ser capaz de balançar. Folhas projetar tal a API muito aberta ao abuso e confundir
Satyan Raina

1
@ TonyD Também não estou sugerindo que a classe Player seja derivada do HashSet. Estou sugerindo que a classe Player deve 'na maioria dos casos' ser implementada usando um HashSet via Composition e isso é totalmente um detalhe de nível de implementação, não um nível de design (é por isso que eu o mencionei como nota de rodapé da minha resposta). Poderia muito bem ser implementado usando uma lista se houver uma justificativa válida para essa implementação. Portanto, para responder à sua pergunta, é necessário fazer uma consulta O (1) por chave? Não. Portanto, NÃO se deve estender um jogador de um HashSet também.
Satyan Raina

50

E se a FootballTeamequipe tiver reservas juntamente com a equipe principal?

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

Como você modelaria isso?

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

O seu SomethingCollectionsemanticamente faz sentido? Faça isso apenas se seu tipo for uma coleção de Something.

No caso de FootballTeamisso não parece certo. A Teamé mais do que a Collection. A Teampode ter treinadores, treinadores, etc, como as outras respostas apontaram.

FootballCollectionparece uma coleção de bolas de futebol ou talvez uma coleção de apetrechos de futebol. TeamCollection, uma coleção de equipes.

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

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

Em suma

Pergunte a si mesmo

  1. É X um Y? ou tem X um Y?

  2. Os nomes das minhas turmas significam o que são?


Se o tipo de cada jogador iria classificá-lo como pertencente a DefensivePlayers, OffensivePlayersou OtherPlayers, pode ser legitimamente útil ter um tipo que pode ser usado pelo código que espera um List<Player>mas os membros também incluídos DefensivePlayers, OffsensivePlayersou SpecialPlayersdo tipo IList<DefensivePlayer>, IList<OffensivePlayer>e IList<Player>. Pode-se usar um objeto separado para armazenar em cache as listas separadas, mas encapsulando-os dentro de um mesmo objeto como a lista principal parece mais limpo [usar a invalidação de uma lista recenseador ...
supercat

... como uma indicação do fato de que a lista principal foi alterada e as sub-listas precisarão ser regeneradas no próximo acesso].
Supercat

2
Embora eu concorde com o que foi exposto, realmente me arrepia ver alguém dar conselhos sobre design e sugerir expor uma Lista <T> concreta com um getter e setter público em um objeto de negócios :(
sara

38

Design> Implementação

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

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

Portanto, suas primeiras perguntas devem ser:

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

Tenha em mente que herança implica um relacionamento "isa" (é a), enquanto composição implica um relacionamento "tem um" (hasa). Escolha o caminho certo para a sua situação, tendo em mente onde as coisas podem ir à medida que seu aplicativo 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 nesse nível na codificação diária. Mas se você está pensando sobre esse tipo de tópico, está pisando nas águas do design. Estar ciente disso pode ser libertador.

Considere os detalhes do projeto

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

FootballTeam.Reverse () faz sentido para você? FootballTeam.ConvertAll <TOutput> () se parece com algo que você deseja?

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

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

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

Se você deseja que seu objeto contenha uma coleção / lista de jogadores (dados) e, portanto, deseja que a coleção ou lista seja uma propriedade ou membro, faça isso de qualquer maneira. Notionally:

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

Você pode sentir que deseja que as pessoas consigam enumerar apenas o conjunto de jogadores, em vez de contá-los, adicioná-los ou removê-los. IEnumerable <Player> é uma opção perfeitamente válida a ser considerada.

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

Qualquer pessoa que tente lhe dizer que uma delas está categoricamente e definitivamente errada em todos os casos está equivocada. Qualquer pessoa que tente lhe dizer que é categórica e definitivamente certa em todos os casos está enganada.

Vá para Implementação

Depois de decidir sobre dados e comportamento, você pode tomar uma decisão sobre a implementação. Isso inclui de quais classes concretas você depende via 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 analisar tudo isso em sua cabeça em um ou dois segundos 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 resultados de jogos para o time? A equipe é intercambiável com o clube, no seu modelo? Nesse caso, e se seu time é uma coleção de jogadores, talvez também seja 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>
{
    ....
}

Não obstante o design, neste momento no C #, você não poderá implementar tudo isso herdando da Lista <T> de qualquer maneira, pois o C # "only" suporta herança única. (Se você já experimentou esse mal-estar no C ++, considere isso uma coisa boa.) A implementação de uma coleção por herança e outra por composição provavelmente parecerá suja. 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 então são mais dolorosas do que confusas. Você pode ver para onde isso está indo; sentir a sensação de pensar nessa avenida pode muito bem dizer que é errado seguir nessa direção (e com ou sem razão, seus colegas também podem se você a implementou dessa maneira!)

A resposta curta (tarde demais)

A diretriz sobre não herdar de classes de coleção não é específica de C #, você a encontrará em muitas linguagens de programação. É sabedoria recebida, não uma lei. Uma razão é que, na prática, considera-se que a composição geralmente vence a herança em termos de compreensibilidade, implementabilidade e manutenibilidade. É mais comum que objetos do mundo real / domínio encontrem relacionamentos "hasa" úteis e consistentes do que relacionamentos "isa" úteis e consistentes, a menos que você tenha um resumo profundo, principalmente com o passar do tempo e com os dados e comportamento precisos dos objetos no código alterar. Isso não deve fazer com que você sempre exclua a herança de classes de coleção; mas pode ser sugestivo.


34

Primeiro de tudo, tem a ver com usabilidade. Se você usar herança, a Teamclasse irá expor comportamentos (métodos) projetados exclusivamente para manipulação de objetos. Por exemplo, AsReadOnly()ou CopyTo(obj)métodos 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ê deseja 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 a seguir. Basta implementar uma lista de jogadores como uma variável privada.


30

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

Isso está 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 
}

19
Isso não explica o porquê . O OP sabe que a maioria dos programadores se sente assim, ele simplesmente não entende por que é importante. Isso realmente está apenas fornecendo informações já em questão.
Servy

2
Essa é a primeira coisa que pensei também, como seus dados foram modelados incorretamente. Portanto, o motivo é que seu modelo de dados está incorreto.
rball

3
Por que não herdar da lista? Porque ele não quer um tipo mais específico de lista.
Mauro Sampietro

2
Não acho que o segundo exemplo esteja correto. Você está expondo o estado e, portanto, está quebrando o encapsulamento. O estado deve estar oculto / interno ao objeto e a maneira de mudar esse estado deve ser por meio de métodos públicos. Exatamente quais métodos são algo a ser determinado a partir dos requisitos de negócios, mas deve ser baseado em "precisa disso agora", não em "pode ​​ser útil em algum momento". Mantenha sua API apertada e você economizará tempo e esforço.
sara

1
Encapsulamento não é o ponto da questão. Eu me concentro em não estender um tipo se você não quiser que esse tipo se comporte de uma maneira mais específica. Esses campos devem ser autoproperties em C # e get / métodos definidos em outros idiomas, mas aqui neste contexto que é totalmente supérfluo
Mauro Sampietro

28

Depende do contexto

Quando você considera seu time como uma lista de jogadores, está projetando a "idéia" de um time de bola para um aspecto: você reduz o "time" às ​​pessoas que vê em campo. Essa projeção está 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 e os antigos, etc.

Semântica pouco clara

Portanto, o problema de 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 muda. Além disso, é difícil expressar qual o contexto que você está usando.

As aulas são extensíveis

Ao usar 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 classes e será armazenada em cache pela máquina virtual. Isso custa desempenho e memória do tempo de execução. 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ê realmente deve apenas usar a 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, não há problema em escrever:

IList<Player> footballTeam = ...

Ao usar o F #, pode ser bom criar uma abreviação de tipo:

type FootballTeam = IList<Player>

Mas quando o contexto é mais amplo ou até pouco claro, você não deve fazer isso. Esse é especialmente o caso quando você cria uma nova classe cujo contexto no qual ela pode ser usada no futuro não é clara. Um sinal de aviso é quando você começa a adicionar atributos adicionais à sua classe (nome da equipe, treinador etc.). Este é um sinal claro de que o contexto em que a classe será usada não é fixo e mudará no futuro. Nesse caso, você não pode considerar o time como uma lista de jogadores, mas deve modelar a lista dos jogadores (atualmente ativos, não lesionados, etc.) como um atributo do time.


27

Deixe-me reescrever sua pergunta. para que você possa ver o assunto de uma perspectiva diferente.

Quando preciso representar um time de futebol, entendo que é basicamente um nome. Como: "The Eagles"

string team = new string();

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 mantenha uma lista de players?

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

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


1
Esse é um bom ponto - pode-se pensar na equipe apenas como um nome. No entanto, se meu aplicativo pretende trabalhar com as ações dos jogadores, esse pensamento é um pouco menos óbvio. Enfim, parece que a questão se resume a composição versus herança no final.
Superbest 16/02

6
Essa é uma maneira meritória de olhar para ela. Como uma nota lateral, considere isso: um homem rico possui vários times de futebol, ele dá um a um amigo como presente, o amigo muda o nome do time, despede o treinador, e substitui todos os jogadores. O time de amigos encontra o time masculino no gramado e, como o time masculino está perdendo, ele diz: "Não acredito que você está me derrotando com o time que eu te dei!" o homem está correto? como você verificaria isso?
user1852503

20

Só porque eu acho que as outras respostas se destacam se um time de futebol "é-a" List<FootballPlayer>ou "tem-a" List<FootballPlayer>, o que realmente não responde a essa pergunta como está escrita.

O OP solicita 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 tem métodos virtuais. Isso é menos problemático em seu próprio código, pois geralmente você pode alternar a implementação com relativamente pouca dor - mas pode ser um negócio 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 da estrutura. E lembre-se de que as diretrizes mencionadas são as " Diretrizes de Design do .NET Framework " e não as " Diretrizes de Design de Aplicativos .NET ". Há uma diferença e, de um modo geral, o design da API pública é muito mais rigoroso.

Se meu projeto atual não tiver e provavelmente não possuir essa API pública, posso ignorar com segurança esta diretriz? Se eu herdar da List e precisar de uma API pública, que dificuldades terei?

Praticamente sim. Convém considerar a lógica por trás disso para verificar se ela se aplica à sua situação, mas se você não estiver criando uma API pública, não precisará se preocupar particularmente com as preocupações da API, como a versão (das quais, este é um subconjunto).

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

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 FootballTeamcomo exemplo - imagine que você não pode adicionar um FootballPlayerse isso fizer com que a equipe ultrapasse o limite de salário. 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 Addporque não é virtual(por razões de desempenho).

Se você estiver em um aplicativo (o que basicamente significa que você e todos os seus chamadores são compilados juntos), agora você pode mudar para o uso IList<T>e corrigir os erros 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ê foi exposto publicamente a terceiros, acabou de fazer uma alteração que causará erros de compilação e / ou tempo de execução.

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


18

Permitir que as pessoas digam

myTeam.subList(3, 5);

faz algum sentido? Caso contrário, não deve ser uma lista.


3
Talvez se você chamasse istomyTeam.subTeam(3, 5);
Sam Leach

3
@SamLeach Supondo que isso seja verdade, você ainda precisará de composição e não herança. Como subListnem vai voltar Teammais.
triturador

1
Isso me convenceu mais do que qualquer outra resposta. +1
FireCubez

16

Depende do comportamento do seu objeto "equipe". Se ele se comportar como uma coleção, pode ser bom representá-la primeiro com uma Lista simples. Em seguida, você pode começar a perceber que continua duplicando o código que itera na lista; Neste ponto, você tem a opção de criar um objeto FootballTeam que envolve a lista de jogadores. A classe FootballTeam se torna o lar de todo o código que itera 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 lista interna ... Mas isso é muito código! O que eu ganho por todo esse trabalho?

Encapsulamento. Seus clientes não precisam saber o que acontece dentro do FootballTeam. Para que todos os seus clientes saibam, isso pode ser implementado consultando a lista de jogadores em um banco de dados. Eles não precisam saber, e isso melhora o 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 de SomeTeam". Você diz "John se juntou ao SomeTeam". Você não adiciona uma letra a "caracteres de uma string", adiciona uma letra a uma string. Você não adiciona um livro aos livros de uma biblioteca, 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.


1
O OP quer entender como MODELAR A REALIDADE, mas depois define um time de futebol como uma lista de jogadores de futebol (o que está errado conceitualmente). Este é o problema. Qualquer outro argumento está enganando-o na minha opinião.
Mauro Sampietro

3
Não concordo que a lista de jogadores esteja errada conceitualmente. Não necessariamente. Como alguém escreveu nesta página, "Todos os modelos estão errados, mas alguns são úteis"
xpmatteo 23/12/14

Os modelos certos são difíceis de obter, uma vez feitos, eles são úteis. Se um modelo pode ser mal utilizado, está errado conceitualmente. stackoverflow.com/a/21706411/711061
Mauro Sampietro

15

Há muitas respostas excelentes aqui, mas quero abordar algo que não vi mencionado: o design orientado a objetos é sobre o fortalecimento de objetos .

Você deseja encapsular todas as suas regras, trabalho adicional e detalhes internos dentro de um objeto apropriado. Dessa maneira, outros objetos que interagem com esse não precisam se preocupar com tudo. Na verdade, você quer dar um passo adiante e impedir ativamente que outros objetos ignorem esses componentes internos.

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ê terá perdido seu controle; por exemplo:

Suponha que você queira diferenciar quando um jogador sair, sabendo se ele se aposentou, renunciou ou foi demitido. Você pode implementar um RemovePlayermétodo que utiliza uma enum de entrada apropriada. No entanto, por herança de List, você não seria capaz de impedir o acesso direto a Remove, RemoveAlle mesmo Clear. Como resultado, você realmente desempoderou sua FootballTeamclasse.


Pensamentos adicionais sobre 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, 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 Playersa todos, 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 de SomeTeam". Você diz "John se juntou ao SomeTeam".

Você está errado quanto ao primeiro ponto: largue a palavra 'lista' e é óbvio que uma equipe tem jogadores.
No entanto, você bate no 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 (possivelmente entre outras coisas) seria chamada Players.Add(...)internamente.


Espero que você possa ver a importância do encapsulamento para o objetivo de capacitar seus objetos. Você deseja permitir que cada classe faça seu trabalho bem, sem medo de interferência de outros objetos.


12

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 caminho útil.

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

- Editado

Minha melhor resposta seria ... depende. A herança de uma lista exporia os clientes desta classe a métodos que podem ser não devem ser expostos, principalmente porque o FootballTeam se parece com uma entidade comercial.

- Edição 2

Sinceramente, não me lembro do que estava me referindo no comentário "não exagere". Embora eu acredite que a mentalidade do KISS seja um bom guia, quero enfatizar que herdar uma classe de negócios da List 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 List é útil. Como escrevi na edição anterior, isso depende. A resposta para cada caso é fortemente influenciada pelo conhecimento, experiência e preferências pessoais.

Agradeço ao @kai por me ajudar a pensar com mais precisão na resposta.


Minha melhor resposta seria ... depende. A herança de uma lista exporia os clientes desta classe a métodos que podem ser não devem ser expostos, principalmente porque o FootballTeam se parece com uma entidade comercial. Não sei se devo editar esta resposta ou postar outra, alguma ideia?
ArturoTena

2
A parte sobre " herdar de a Listexporia os clientes dessa classe a métodos que podem ser não devem ser expostos " é precisamente o que torna a herança de Listum absoluto não-não. Uma vez expostos, eles gradualmente são abusados ​​e mal utilizados ao longo do tempo. Economizar tempo agora "desenvolvendo-se economicamente" pode facilmente levar a dez vezes mais economias perdidas no futuro: depuração dos abusos e, eventualmente, refatoração para corrigir a herança. O princípio YAGNI também pode ser entendido como significando: Você não precisará de todos esses métodos List, portanto não os exponha.
Desiludido

3
"não faça engenharia demais. Quanto menos código você escrever, menos código precisará para depurar." <- Eu acho que isso é enganoso. O encapsulamento e a composição sobre a herança NÃO são excessivos em engenharia e não causam mais necessidade de depuração. Ao encapsular, você está LIMITANDO o número de maneiras que os clientes podem usar (e abusar) de sua classe e, portanto, você tem menos pontos de entrada que precisam de testes e validação de entrada. Herdar de Listporque é rápido e fácil e, portanto, levaria a menos bugs está completamente errado, é apenas um projeto ruim e um projeto ruim leva a muito mais erros do que "engenharia excedente".
sara

@kai Eu concordo com você em todos os pontos. Sinceramente, não me lembro do que estava me referindo no comentário "não exagere". OTOH, acredito que há um número limitado de casos em que simplesmente herdar da List é útil. Como escrevi na edição posterior, isso depende. A resposta para cada caso é fortemente influenciada pelo conhecimento, experiência e preferências pessoais. Como tudo na vida. ;-)
ArturoTena 15/15

11

Isso me lembra do "É um" versus "tem um" tradeoff. Às vezes, é mais fácil e faz mais sentido herdar diretamente de uma super classe. Outras vezes, faz mais sentido criar uma classe independente e incluir a classe da qual você teria herdado como uma 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 advir da herança da classe.

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


9

Meu segredo sujo: eu não ligo para o que as pessoas dizem, e eu faço. O .NET Framework é espalhado com "XxxxCollection" (UIElementCollection para o topo 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 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 um amplo acordo entre a comunidade. Em vez de perguntar à comunidade se a culpa será sua quando você se perguntar, o que é mais legível e sustentável?

team.Players.ByName("Nicolas") 

ou

team.ByName("Nicolas")

Realmente. Você tem alguma dúvida? Agora talvez você precise jogar com outras restrições técnicas que o impedem de usar a Lista <T> no 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ê, certamente é uma "prática recomendada" vinda do nada.


9
Embora seja importante ter a coragem e a iniciativa de desafiar a sabedoria aceita, quando apropriado, acho que é sábio entender primeiro por que a sabedoria aceita passou a ser aceita em primeiro lugar, antes de começar a desafiá-la. -1 porque "Ignore essa diretriz!" não é uma boa resposta para "Por que essa diretriz existe?" Aliás, a comunidade não apenas "me culpou" neste caso, mas forneceu uma explicação satisfatória que conseguiu me convencer.
Superbest

3
Na verdade, quando nenhuma explicação é feita, seu único caminho é ultrapassar os limites e testar não ter medo de infração. Agora. Pergunte a si mesmo, mesmo que a Lista <T> não seja uma boa ideia. Quão doloroso seria mudar a herança da Lista <T> para a Coleção <T>? Um palpite: Menos tempo do que postar no fórum, qualquer que seja o tamanho do seu código com ferramentas de refatoração. Agora é uma boa pergunta, mas não prática.
Nicolas Dorier 12/02

Para esclarecer: certamente não estou esperando que as bênçãos do SO herdem o que eu quiser, mas o objetivo da minha pergunta era entender as considerações que deveriam entrar nessa decisão e por que tantos programadores experientes parecem ter decidiu o oposto do que eu fiz. Collection<T>não é o mesmo, List<T>portanto, pode ser necessário um pouco de trabalho para verificar em um projeto de grande porte.
Superbest

2
team.ByName("Nicolas")significa "Nicolas"é o nome da equipe.
Sam Leach

1
"não adicione uma restrição que não deveria existir" Au contraire, não exponha nada que você não tenha um bom motivo para expor. Isso não é purismo ou fanatismo cego, nem é a última tendência da moda em design. É design básico orientado a objetos 101, nível iniciante. Não é segredo porque essa "regra" existe, é o resultado de várias décadas de experiência.
sara

8

O que as diretrizes dizem é que a API pública não deve revelar a decisão interna de design de se você está usando uma lista, um conjunto, um dicionário, uma árvore ou qualquer outra coisa. 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 conforme a necessidade. Isso permite que você altere sua decisão e use uma estrutura de dados diferente sem afetar a interface pública.


Em retrospecto, após a explicação de @EricLippert e outros, você realmente deu uma ótima resposta para a parte da API da minha pergunta - além do que você disse, se eu fizer class FootballTeam : List<FootballPlayers>, os usuários da minha classe poderão dizer que eu ' que herdei List<T>usando reflexão, vendo os List<T>métodos que não fazem sentido para um time de futebol e conseguindo usá FootballTeam- List<T>lo, então eu estaria revelando detalhes da implementação ao cliente (desnecessariamente).
Superbest

7

Quando eles dizem que List<T>é "otimizado", acho que eles querem dizer que ele não possui recursos como métodos virtuais que são um pouco mais caros. Portanto, o problema é que, depois de expor List<T>em sua API pública , você perde a capacidade de impor regras de negócios ou personalizar sua funcionalidade posteriormente. Mas se você estiver usando essa classe herdada como interna em seu projeto (em vez de potencialmente exposta a milhares de seus clientes / parceiros / outras equipes como API), pode ser bom se economizar seu tempo e a funcionalidade que você deseja duplicado. A vantagem de herdar List<T>é que você elimina muitos códigos de invólucros estúpidos que nunca serão personalizados em um futuro próximo. Além disso, se você quiser que sua classe tenha explicitamente a mesma semântica exata deList<T> durante toda a vida útil de suas APIs, também pode ser bom.

Costumo ver muitas pessoas fazendo toneladas de trabalho extra apenas por causa da regra da FxCop, ou o blog de alguém diz que é uma prática "ruim". Muitas vezes, isso transforma o código em design de estranheza padrão de palooza. Assim como ocorre com muitas diretrizes, trate-as como diretrizes que podem ter exceções.


4

Embora 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 Teamclasse suporte 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
{
    ...
}

3

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

Essa é 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 minúsculo e não mais que 2 desenvolvedores. Isso economizará algum tempo.

Para aplicativos pequenos, considere também escolher outro idioma menos estrito. Ruby, JavaScript - qualquer coisa que permita escrever menos código.


3

Eu só queria acrescentar que Bertrand Meyer, o inventor de Eiffel e o design por contrato, teriam Teamherdado List<Player>sem nem sequer piscar as pálpebras.

Em seu livro, Construção de software orientada a objetos , ele discute a implementação de um sistema GUI em que janelas retangulares podem ter janelas filho. 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 . No C #, quando você subclasse, 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. No Eiffel, no entanto, você pode modificar os nomes dos métodos públicos, para renomear Adde Removepara Hiree Fireno seu Team. Se uma instância de Teamfor upcast de volta List<Player>, o chamador irá usá-la Adde Removemodificá-la, mas seus métodos virtuais Hiree Fireserão chamados.


1
Aqui o problema não é herança múltipla, mixins (etc.) ou como lidar com sua ausência. Em qualquer idioma, mesmo na linguagem humana, uma equipe não é uma lista de pessoas, mas composta de uma lista de pessoas. Este é um conceito abstrato. Se Bertrand Meyer ou alguém gerencia uma equipe subclassificando List, está fazendo algo errado. Você deve subclassificar uma lista se desejar um tipo mais específico de lista. Espero que você concorde.
Mauro Sampietro 07/07

1
Isso depende do que a herança é para você. Por si só, é uma operação abstrata, não significa que duas classes estão em um relacionamento "é um tipo especial de", mesmo que seja a interpretação principal. No entanto, a herança pode ser tratada como um mecanismo de reutilização da implementação, se o design da linguagem a suportar, mesmo que a composição seja a alternativa preferida atualmente.
Alexey

2

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 a respeito - nome, emblema, coleção de equipe administrativa / administrativa, coleção de equipe técnica e coleção de jogadores. Portanto, corretamente, sua classe 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 a StringCollection especializada, oferece algumas outras facilidades - como validação e verificações antes de os objetos serem adicionados ou removidos do armazenamento interno.

Talvez, a noção de apostadores PlayerCollection se adapte à sua abordagem preferida?

public class PlayerCollection : Collection<Player> 
{ 
}

E então o FootballTeam pode ficar 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();
}

2

Preferir interfaces sobre classes

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

Herança quebra Encapsulamento

Derivando do encapsulamento de quebras de classes :

  • 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 dificulta a refatoração do seu código.

As classes são um detalhe de implementação

Classes são detalhes de implementação que devem ser ocultados de outras partes do seu código. Em resumo, a System.Listé uma implementação específica de um tipo de dado 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 complicado. 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 pelo índice.

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

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

Como escolher interfaces

Quando você pensa em "lista", deve começar dizendo a si mesmo: "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 guiar esse processo:

  • Preciso ter a contagem? Caso contrário, considere implementarIEnumerable<T>
  • Essa coleção será alterada depois de inicializada? Se não considerar IReadonlyList<T>.
  • É importante que eu possa acessar itens por índice? ConsiderarICollection<T>
  • A ordem na qual adiciono itens à coleção é importante? Talvez seja um ISet<T>?
  • Se você realmente quer isso, vá em frente e implemente IList<T>.

Dessa forma, você não acoplará outras partes do código aos detalhes de implementação da sua coleção de jogadores de beisebol e poderá alterar como ele será implementado, desde que 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 Boilerplate

A implementação de 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 de membro, se necessário.

Dito isto, se você achar que está escrevendo muitos clichês, isso é potencialmente porque você está expondo mais funções do que deveria. É o mesmo motivo que você não deve herdar de uma classe.

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


2

Problemas com serialização

Está faltando um aspecto. Classes herdadas da Lista <> não podem ser serializadas corretamente usando XmlSerializer . Nesse caso, o DataContractSerializer deve ser usado ou uma implementação de serialização própria é necessária.

public class DemoList : List<Demo>
{
    // using XmlSerializer this properties won't be seralized:
    string AnyPropertyInDerivedFromList { get; set; }     
}

public class Demo
{
    // this properties will be seralized
    string AnyPropetyInDemo { get; set; }  
}

Leitura adicional: Quando uma classe é herdada da Lista <>, o XmlSerializer não serializa outros atributos


0

Quando é aceitável?

Para citar Eric Lippert:

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

Por exemplo, você está cansado da ausência do AddRangemétodo em IList<T>:

public interface IMoreConvenientListInterface<T> : IList<T>
{
    void AddRange(IEnumerable<T> collection);
}

public class MoreConvenientList<T> : List<T>, IMoreConvenientListInterface<T> { }
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.